update ucx dav-2 tip

Sat, 08 Nov 2025 23:06:11 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Sat, 08 Nov 2025 23:06:11 +0100
branch
dav-2
changeset 889
42cdbf9bbd49
parent 887
26541c37b619

update ucx

dav/Makefile file | annotate | diff | comparison | revisions
dav/config.c file | annotate | diff | comparison | revisions
dav/main.c file | annotate | diff | comparison | revisions
dav/main.h file | annotate | diff | comparison | revisions
dav/obj.mk file | annotate | diff | comparison | revisions
dav/optparser.c file | annotate | diff | comparison | revisions
dav/optparser.h file | annotate | diff | comparison | revisions
dav/pull.c file | annotate | diff | comparison | revisions
dav/pull.h file | annotate | diff | comparison | revisions
dav/push.c file | annotate | diff | comparison | revisions
dav/push.h file | annotate | diff | comparison | revisions
dav/sopt.c file | annotate | diff | comparison | revisions
dav/sopt.h file | annotate | diff | comparison | revisions
dav/sync.c file | annotate | diff | comparison | revisions
dav/sync.h file | annotate | diff | comparison | revisions
dav/sync_main.c file | annotate | diff | comparison | revisions
dav/sync_main.h file | annotate | diff | comparison | revisions
dav/syncdir.c file | annotate | diff | comparison | revisions
dav/syncdir.h file | annotate | diff | comparison | revisions
dav/xattrtool.c file | annotate | diff | comparison | revisions
libidav/pwdstore.c file | annotate | diff | comparison | revisions
libidav/resource.c file | annotate | diff | comparison | revisions
libidav/webdav.c file | annotate | diff | comparison | revisions
libidav/webdav.h file | annotate | diff | comparison | revisions
test/Makefile file | annotate | diff | comparison | revisions
test/base64.c file | annotate | diff | comparison | revisions
test/base64.h file | annotate | diff | comparison | revisions
test/crypto.c file | annotate | diff | comparison | revisions
test/crypto.h file | annotate | diff | comparison | revisions
test/helper.c file | annotate | diff | comparison | revisions
test/helper.h file | annotate | diff | comparison | revisions
test/main.c file | annotate | diff | comparison | revisions
test/test.c file | annotate | diff | comparison | revisions
test/test.h file | annotate | diff | comparison | revisions
test/test_finfo.c file | annotate | diff | comparison | revisions
test/test_finfo.h file | annotate | diff | comparison | revisions
test/test_resource.c file | annotate | diff | comparison | revisions
test/test_resource.h file | annotate | diff | comparison | revisions
test/utils.c file | annotate | diff | comparison | revisions
test/utils.h file | annotate | diff | comparison | revisions
ucx/Makefile file | annotate | diff | comparison | revisions
ucx/allocator.c file | annotate | diff | comparison | revisions
ucx/array_list.c file | annotate | diff | comparison | revisions
ucx/compare.c file | annotate | diff | comparison | revisions
ucx/cx/allocator.h file | annotate | diff | comparison | revisions
ucx/cx/array_list.h file | annotate | diff | comparison | revisions
ucx/cx/buffer.h file | annotate | diff | comparison | revisions
ucx/cx/collection.h file | annotate | diff | comparison | revisions
ucx/cx/common.h file | annotate | diff | comparison | revisions
ucx/cx/compare.h file | annotate | diff | comparison | revisions
ucx/cx/hash_key.h file | annotate | diff | comparison | revisions
ucx/cx/hash_map.h file | annotate | diff | comparison | revisions
ucx/cx/iterator.h file | annotate | diff | comparison | revisions
ucx/cx/json.h file | annotate | diff | comparison | revisions
ucx/cx/kv_list.h file | annotate | diff | comparison | revisions
ucx/cx/linked_list.h file | annotate | diff | comparison | revisions
ucx/cx/list.h file | annotate | diff | comparison | revisions
ucx/cx/map.h file | annotate | diff | comparison | revisions
ucx/cx/mempool.h file | annotate | diff | comparison | revisions
ucx/cx/printf.h file | annotate | diff | comparison | revisions
ucx/cx/properties.h file | annotate | diff | comparison | revisions
ucx/cx/streams.h file | annotate | diff | comparison | revisions
ucx/cx/string.h file | annotate | diff | comparison | revisions
ucx/cx/test.h file | annotate | diff | comparison | revisions
ucx/cx/tree.h file | annotate | diff | comparison | revisions
ucx/hash_key.c file | annotate | diff | comparison | revisions
ucx/hash_map.c file | annotate | diff | comparison | revisions
ucx/iterator.c file | annotate | diff | comparison | revisions
ucx/json.c file | annotate | diff | comparison | revisions
ucx/kv_list.c file | annotate | diff | comparison | revisions
ucx/linked_list.c file | annotate | diff | comparison | revisions
ucx/list.c file | annotate | diff | comparison | revisions
ucx/map.c file | annotate | diff | comparison | revisions
ucx/mempool.c file | annotate | diff | comparison | revisions
ucx/properties.c file | annotate | diff | comparison | revisions
ucx/string.c file | annotate | diff | comparison | revisions
ucx/tree.c file | annotate | diff | comparison | revisions
--- a/dav/Makefile	Tue Oct 14 21:02:26 2025 +0200
+++ b/dav/Makefile	Sat Nov 08 23:06:11 2025 +0100
@@ -28,7 +28,36 @@
 
 include ../config.mk
 
-include obj.mk
+DAV_SRC  = main.c
+DAV_SRC += config.c
+DAV_SRC += optparser.c
+DAV_SRC += error.c
+DAV_SRC += assistant.c
+DAV_SRC += tar.c
+DAV_SRC += system.c
+DAV_SRC += libxattr.c
+DAV_SRC += finfo.c
+DAV_SRC += connect.c
+
+SYNC_SRC  = sync.c
+SYNC_SRC += config.c
+SYNC_SRC += scfg.c
+SYNC_SRC += sopt.c
+SYNC_SRC += db.c
+SYNC_SRC += error.c
+SYNC_SRC += assistant.c
+SYNC_SRC += libxattr.c
+SYNC_SRC += finfo.c
+SYNC_SRC += tags.c
+SYNC_SRC += system.c
+SYNC_SRC += connect.c
+
+XATTR_SRC  = xattrtool.c
+XATTR_SRC += libxattr.c
+
+DAV_OBJ = $(DAV_SRC:%.c=../build/tool/%$(OBJ_EXT))
+SYNC_OBJ = $(SYNC_SRC:%.c=../build/tool/%$(OBJ_EXT))
+XATTR_OBJ = $(XATTR_SRC:%.c=../build/tool/%$(OBJ_EXT))
 
 DAV_BIN = ../build/bin/dav$(APP_EXT)
 DAV_SYNC_BIN = ../build/bin/dav-sync$(APP_EXT)
@@ -36,13 +65,13 @@
 
 all: ../build/bin/dav ../build/bin/dav-sync ../build/bin/xattrtool
 
-$(DAV_BIN): $(DAV_MAIN_OBJ) $(DAV_OBJ) $(COMMON_OBJ) ../build/lib/libidav$(LIB_EXT)
-	$(CC) -o $(DAV_BIN) $(DAV_MAIN_OBJ) $(DAV_OBJ) $(COMMON_OBJ) \
+$(DAV_BIN): $(DAV_OBJ) ../build/lib/libidav$(LIB_EXT)
+	$(CC) -o $(DAV_BIN) $(DAV_OBJ) \
 		../build/lib/libidav$(LIB_EXT) ../build/lib/libucx$(LIB_EXT)  \
 		$(LDFLAGS) $(DAV_LDFLAGS)
 
-$(DAV_SYNC_BIN): $(SYNC_MAIN_OBJ) $(SYNC_OBJ) $(COMMON_OBJ) ../build/lib/libidav$(LIB_EXT)
-	$(CC) -o $(DAV_SYNC_BIN) $(SYNC_MAIN_OBJ) $(COMMON_OBJ) $(SYNC_OBJ) \
+$(DAV_SYNC_BIN): $(SYNC_OBJ) ../build/lib/libidav$(LIB_EXT)
+	$(CC) -o $(DAV_SYNC_BIN) $(SYNC_OBJ) \
 		../build/lib/libidav$(LIB_EXT) ../build/lib/libucx$(LIB_EXT)  \
 		$(LDFLAGS) $(DAV_LDFLAGS)
 
--- a/dav/config.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/dav/config.c	Sat Nov 08 23:06:11 2025 +0100
@@ -31,7 +31,6 @@
 #include <string.h>
 #include <sys/types.h>
 #include <cx/hash_map.h>
-#include <cx/streams.h>
 #include <errno.h>
 #include <libxml/tree.h>
 
@@ -42,6 +41,8 @@
 #include <libidav/config.h>
 #include <libidav/pwdstore.h>
 
+#include <cx/streams.h>
+
 #define xstreq(a,b) xmlStrEqual(BAD_CAST a, BAD_CAST b)
 #define xstrEQ(a,b) !xmlStrcasecmp(BAD_CAST a, BAD_CAST b)
 
--- a/dav/main.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/dav/main.c	Sat Nov 08 23:06:11 2025 +0100
@@ -127,7 +127,7 @@
 int dav_main(int argc, char **argv) {
     if(argc < 2) {
         fprintf(stderr, "Missing command\n");
-        dav_sync_print_usage(argv[0]);
+        print_usage(argv[0]);
         return -1;
     }
     
@@ -136,7 +136,7 @@
     char *cmd = argv[1];
     CmdArgs *args = cmd_parse_args(argc - 2, argv + 2);
     if(!args) {
-        dav_sync_print_usage(argv[0]);
+        print_usage(argv[0]);
         return -1;
     }
     if(cmd_getoption(args, "noinput")) {
@@ -247,7 +247,7 @@
         } else if(!strcasecmp(cmd, "complete")) {
             ret = cmd_complete(args);
         } else {
-            dav_sync_print_usage(argv[0]);
+            print_usage(argv[0]);
         }
     }
     
@@ -298,7 +298,7 @@
     return NULL;
 }
 
-void dav_sync_print_usage(char *cmd) {
+void print_usage(char *cmd) {
     fprintf(stderr, "Usage: %s command [options] arguments...\n\n", cmd);
     
     fprintf(stderr, "Commands:\n");
--- a/dav/main.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/dav/main.h	Sat Nov 08 23:06:11 2025 +0100
@@ -59,7 +59,7 @@
 
 typedef int(*getfunc)(DavCfgRepository *, GetResource *, CmdArgs *, void *);
     
-void dav_sync_print_usage(char *cmd);
+void print_usage(char *cmd);
 
 int update_progress(DavResource *res, int64_t total, int64_t now, Progress *p);
 void download_progress(DavResource *res, int64_t total, int64_t now, void *data);
--- a/dav/obj.mk	Tue Oct 14 21:02:26 2025 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,61 +0,0 @@
-#
-# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
-#
-# Copyright 2025 Olaf Wintermann. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-#   1. Redistributions of source code must retain the above copyright
-#      notice, this list of conditions and the following disclaimer.
-#
-#   2. Redistributions in binary form must reproduce the above copyright
-#      notice, this list of conditions and the following disclaimer in the
-#      documentation and/or other materials provided with the distribution.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
-# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
-# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
-# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-# POSSIBILITY OF SUCH DAMAGE.
-#
-
-DAV_MAIN_SRC  = main.c
-
-SYNC_MAIN_SRC  = sync_main.c
-
-COMMON_SRC  = config.c
-COMMON_SRC += error.c
-COMMON_SRC += assistant.c
-COMMON_SRC += libxattr.c
-COMMON_SRC += finfo.c
-COMMON_SRC += connect.c
-COMMON_SRC += system.c
-COMMON_SRC += optparser.c
-
-DAV_SRC = tar.c
-
-SYNC_SRC += sync.c
-SYNC_SRC += syncdir.c
-SYNC_SRC += scfg.c
-SYNC_SRC += db.c
-SYNC_SRC += tags.c
-SYNC_SRC += pull.c
-SYNC_SRC += push.c
-
-XATTR_SRC  = xattrtool.c
-XATTR_SRC += libxattr.c
-
-DAV_OBJ = $(DAV_SRC:%.c=../build/tool/%$(OBJ_EXT))
-SYNC_OBJ = $(SYNC_SRC:%.c=../build/tool/%$(OBJ_EXT))
-COMMON_OBJ = $(COMMON_SRC:%.c=../build/tool/%$(OBJ_EXT))
-XATTR_OBJ = $(XATTR_SRC:%.c=../build/tool/%$(OBJ_EXT))
-	
-DAV_MAIN_OBJ = $(DAV_MAIN_SRC:%.c=../build/tool/%$(OBJ_EXT))
-SYNC_MAIN_OBJ = $(SYNC_MAIN_SRC:%.c=../build/tool/%$(OBJ_EXT))
--- a/dav/optparser.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/dav/optparser.c	Sat Nov 08 23:06:11 2025 +0100
@@ -31,6 +31,7 @@
 #include <string.h>
 
 #include "optparser.h"
+#include "sopt.h"
 
 #include <cx/hash_map.h>
 
@@ -215,86 +216,6 @@
     return a;
 }
 
-CmdArgs* cmd_parse_sync_args(int argc, char **argv) {
-    CmdArgs *a = malloc(sizeof(CmdArgs));
-    a->options = cxHashMapCreate(cxDefaultAllocator, CX_STORE_POINTERS, 16);
-    a->argv = calloc(argc, sizeof(char*));
-    a->argc = 0;
-    
-    const char *NOARG = "";
-    
-    char *option = NULL;
-    char optchar = 0;
-    for(int i=0;i<argc;i++) {
-        char *arg = argv[i];
-        size_t len = strlen(arg);
-        if(len > 1 && arg[0] == '-') {
-            for(int c=1;c<len;c++) {
-                switch(arg[c]) {
-                    default: {
-                        fprintf(stderr, "Unknown option -%c\n\n", arg[c]);
-                        cmd_args_free(a);
-                        return NULL;
-                    }
-                    case 'c': {
-                        cmd_map_put(a->options, "conflict", NOARG);
-                        break;
-                    }
-                    case 'l': {
-                        cmd_map_put(a->options, "lock", NOARG);
-                        break;
-                    }
-                    case 'd': {
-                        cmd_map_put(a->options, "nolock", NOARG);
-                        break;
-                    }
-                    case 'r': {
-                        cmd_map_put(a->options, "remove", NOARG);
-                        break;
-                    }
-                    case 'v': {
-                        cmd_map_put(a->options, "verbose", NOARG);
-                        break;
-                    }
-                    case 's': {
-                        option = "syncdir";
-                        optchar = 's';
-                        break;
-                    }
-                    case 't': {
-                        option = "tags";
-                        optchar = 't';
-                        break;
-                    }
-                    case 'V': {
-                        option = "version";
-                        optchar = 'V';
-                        break;
-                    }
-                    case 'R': {
-                        cmd_map_put(a->options, "restore-removed", NOARG);
-                        break;
-                    }
-                    case 'M': {
-                        cmd_map_put(a->options, "restore-modified", NOARG);
-                        break;
-                    }
-                    case 'S': {
-                        cmd_map_put(a->options, "snapshot", NOARG);
-                    }
-                }
-            }
-        } else if(option) {
-            cmd_map_put(a->options, option, arg);
-            option = NULL;
-        } else {
-            a->argv[a->argc++] = arg;
-        }
-    }
-    
-    return a;
-}
-
 char* cmd_getoption(CmdArgs *arg, const char *name) {
     return cxMapGet(arg->options, cx_hash_key_str(name));
 }
--- a/dav/optparser.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/dav/optparser.h	Sat Nov 08 23:06:11 2025 +0100
@@ -37,7 +37,6 @@
 #endif
 
 CmdArgs* cmd_parse_args(int argc, char **argv);
-CmdArgs* cmd_parse_sync_args(int argc, char **argv);
 char* cmd_getoption(CmdArgs *arg, const char *name);
 
 void cmd_args_free(CmdArgs *args);
--- a/dav/pull.c	Tue Oct 14 21:02:26 2025 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,287 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2025 Olaf Wintermann. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#include "pull.h"
-#include "syncdir.h"
-
-#include <libidav/utils.h>
-#include <errno.h>
-#include <string.h>
-
-/*
- * strcmp version that works with NULL pointers
- */
-static int nullstrcmp(const char *s1, const char *s2) {
-    if(!s1 && s2) {
-        return -1;
-    }
-    if(s1 && !s2) {
-        return 1;
-    }
-    if(!s1 && !s2) {
-        return 0;
-    }
-    return strcmp(s1, s2);
-}
-
-static char* nullstrdup(const char *s) {
-    return s ? strdup(s) : NULL;
-}
-
-
-
-RemoteChangeType resource_get_remote_change(
-        CmdArgs *a,
-        DavResource *res,
-        SyncDirectory *dir,
-        SyncDatabase *db)
-{
-    DavBool update_db = FALSE;
-    
-    char *etag = dav_get_string_property(res, "D:getetag");
-    if(!etag) {
-        fprintf(stderr, "Error: resource %s has no etag\n", res->path);
-        return REMOTE_NO_CHANGE;
-    }
-    char *hash = sync_get_content_hash(res);
-      
-    DavBool issplit = dav_get_property(res, "idav:split") ? TRUE : FALSE;
-    if(issplit) {
-        util_remove_trailing_pathseparator(res->path);
-    }
-    DavBool iscollection = res->iscollection && !issplit;
-    
-    RemoteChangeType type = cmd_getoption(a, "conflict") ?
-            REMOTE_CHANGE_MODIFIED : REMOTE_CHANGE_CONFLICT_LOCAL_MODIFIED;
-    
-    LocalResource *local = cxMapGet(db->resources, dav_resource_path_key(res));
-    char *local_path = syncdir_create_local_path(dir, res->path);
-    
-    char *link = SYNC_SYMLINK(dir) ? 
-            dav_get_string_property_ns(res, DAV_PROPS_NS, "link") : NULL;
-    
-    SYS_STAT s;
-    DavBool exists = 1;
-    if(sys_stat(local_path, &s)) {
-        if(errno != ENOENT) {
-            fprintf(stderr, "Cannot stat file: %s\n", local_path);
-            free(local_path);
-            return REMOTE_NO_CHANGE;
-        }
-        exists = 0;
-    }
-    
-    RemoteChangeType ret = REMOTE_NO_CHANGE;
-    if(iscollection) {
-        if(!exists) {
-            ret = REMOTE_CHANGE_MKDIR;
-        } else if(local && S_ISDIR(s.st_mode)) {
-            local->isdirectory = 1; // make sure isdirectory is set
-        } else {
-            // set change to REMOTE_CHANGE_MKDIR, which will fail later
-            ret = REMOTE_CHANGE_MKDIR;
-        }
-    } else if(local) {
-        DavBool nochange = FALSE;
-        if(SYNC_SYMLINK(dir) && nullstrcmp(link, local->link_target)) {
-            ret = REMOTE_CHANGE_LINK;
-            nochange = TRUE;
-            
-            if(local->link_target) {
-                LocalResource *local2 = local_resource_new(dir, db, local->path);
-                if(type == REMOTE_CHANGE_CONFLICT_LOCAL_MODIFIED && nullstrcmp(local->link_target, local2->link_target)) {
-                    ret = REMOTE_CHANGE_CONFLICT_LOCAL_MODIFIED;
-                }            
-                local_resource_free(local2);
-
-                if(!nullstrcmp(link, local->link_target)) {
-                    ret = REMOTE_NO_CHANGE;
-                    update_db = TRUE;
-                }
-            } 
-        } else if(issplit && local->hash && hash) {
-            if(!strcmp(local->hash, hash)) {
-                // resource is already up-to-date on the client
-                nochange = TRUE;
-            }
-        } else if(local->etag) {
-            cxstring e = cx_str(etag);
-            if(cx_strprefix(e, CX_STR("W/"))) {
-                e = cx_strsubs(e, 2);
-            }
-            if(!strcmp(e.ptr, local->etag)) {
-                // resource is already up-to-date on the client
-                nochange = TRUE;
-            }
-        }
-        
-        if(!nochange) {
-            if(!(exists && s.st_mtime != local->last_modified)) {
-                type = REMOTE_CHANGE_MODIFIED;
-            }
-            ret = type;
-        }
-    } else if(link) {
-        // new file is a link
-        ret = REMOTE_CHANGE_LINK;
-        
-        if(exists && type == REMOTE_CHANGE_CONFLICT_LOCAL_MODIFIED) {
-            // a file with the same name already exists
-            // if it is a link, compare the targets
-            LocalResource *local2 = local_resource_new(dir, db, res->path);
-            if(local2) {
-                if(local2->link_target) {
-                    if(strcmp(link, local2->link_target)) {
-                        ret = REMOTE_CHANGE_CONFLICT_LOCAL_MODIFIED;
-                    }
-                } else {
-                    ret = REMOTE_CHANGE_CONFLICT_LOCAL_MODIFIED;
-                }
-                
-                local_resource_free(local2);
-            }
-        }
-        
-    } else if(exists) {
-        ret = type;
-    } else {
-        ret = REMOTE_CHANGE_NEW;
-    }
-     
-    // if hashing is enabled we can compare the hash of the remote file
-    // with the local file to test if a file is really modified
-    char *update_hash = NULL;
-    if (!iscollection &&
-        !link &&
-        (ret == REMOTE_CHANGE_MODIFIED || ret == REMOTE_CHANGE_CONFLICT_LOCAL_MODIFIED) &&
-        exists &&
-        hash &&
-        !dir->pull_skip_hashing)
-    {
-        // because rehashing a file is slow, there is a config element for
-        // disabling this (pull-skip-hashing)
-        char *local_hash = util_file_hash(local_path);
-        if(local_hash) {
-            if(!strcmp(hash, local_hash)) {
-                ret = REMOTE_NO_CHANGE;
-                update_db = TRUE;
-                update_hash = local_hash;
-                
-                // if local already exists, update the hash here
-                // because it is possible that there are metadata updates
-                // and in this case the db will updated later and needs
-                // the current hash
-                if(local) {
-                    if(local->hash) {
-                        free(local->hash);
-                    }
-                    local->hash = local_hash;
-                }
-            } else {
-                free(local_hash);
-            }
-        }
-    }
-    
-    // if a file is not modified, check if the metadata has changed
-    while(ret == REMOTE_NO_CHANGE && local) {
-        // check if tags have changed
-        if(dir->tagconfig) {
-            DavXmlNode *tagsprop = dav_get_property_ns(res, DAV_PROPS_NS, "tags");
-            CxList *remote_tags = NULL;
-            if(tagsprop) {
-                remote_tags = parse_dav_xml_taglist(tagsprop);
-            }
-            char *remote_hash = create_tags_hash(remote_tags);
-            if(nullstrcmp(remote_hash, local->remote_tags_hash)) {
-                ret = REMOTE_CHANGE_METADATA;
-            }
-            if(remote_hash) {
-                free(remote_hash);
-            }
-            free_taglist(remote_tags);
-            
-            if(ret == REMOTE_CHANGE_METADATA) {
-                break;
-            }
-        }
-        
-        // check if extended attributes have changed
-        if((dir->metadata & FINFO_XATTR) == FINFO_XATTR) {
-            DavXmlNode *xattr = dav_get_property_ns(res, DAV_PROPS_NS, "xattributes");
-            char *xattr_hash = get_xattr_hash(xattr);
-            if(nullstrcmp(xattr_hash, local->xattr_hash)) {
-                ret = REMOTE_CHANGE_METADATA;
-                break;
-            }
-        } 
-        
-        // check if finfo has changed
-        DavXmlNode *finfo = dav_get_property_ns(res, DAV_PROPS_NS, "finfo");
-        if((dir->metadata & FINFO_MODE) == FINFO_MODE) {
-            FileInfo f;
-            finfo_get_values(finfo, &f);
-            if(f.mode_set && f.mode != local->mode) {
-                ret = REMOTE_CHANGE_METADATA;
-                break;
-            }
-        }
-        
-        break;
-    }
-    
-    // if update_db is set, a file was modified on the server, but we already
-    // have the file content, but we need to update the db
-    if(ret == REMOTE_NO_CHANGE && update_db) {
-        if(!local) {
-            local = calloc(1, sizeof(LocalResource));
-            local->path = strdup(res->path);
-            
-            cxMapPut(db->resources, cx_hash_key_str(local->path), local);
-        }
-        
-        // update local res
-        SYS_STAT statdata;
-        if(!sys_stat(local_path, &statdata)) {
-            sync_set_metadata_from_stat(local, &statdata);
-        } else {
-            fprintf(stderr, "stat failed for file: %s : %s", local_path, strerror(errno));
-        }
-        local_resource_set_etag(local, etag);
-        if(!local->hash) {
-            local->hash = update_hash;
-        } // else: hash already updated
-        if(link) {
-            free(local->link_target);
-            local->link_target = link;
-        }
-    }
-        
-    free(local_path);
-    return ret;
-}
--- a/dav/pull.h	Tue Oct 14 21:02:26 2025 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,46 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2025 Olaf Wintermann. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#ifndef PULL_H
-#define PULL_H
-
-#include "sync.h"
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-
-
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /* PULL_H */
-
--- a/dav/push.c	Tue Oct 14 21:02:26 2025 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2025 Olaf Wintermann. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#include "push.h"
-
--- a/dav/push.h	Tue Oct 14 21:02:26 2025 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,44 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2025 Olaf Wintermann. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#ifndef PUSH_H
-#define PUSH_H
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-
-
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /* PUSH_H */
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dav/sopt.c	Sat Nov 08 23:06:11 2025 +0100
@@ -0,0 +1,131 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2018 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "sopt.h"
+
+#include <cx/hash_map.h>
+
+void cmd_args_free(CmdArgs *args) {
+    if(args) {
+        cxMapFree(args->options);
+        free(args->argv);
+        free(args);
+    }
+}
+
+static void cmd_map_put(CxMap *map, const char *key, const void *value) {
+    cxMapPut(map, key, (void*)value);
+}
+
+CmdArgs* cmd_parse_args(int argc, char **argv) {
+    CmdArgs *a = malloc(sizeof(CmdArgs));
+    a->options = cxHashMapCreate(cxDefaultAllocator, CX_STORE_POINTERS, 16);
+    a->argv = calloc(argc, sizeof(char*));
+    a->argc = 0;
+    
+    const char *NOARG = "";
+    
+    char *option = NULL;
+    char optchar = 0;
+    for(int i=0;i<argc;i++) {
+        char *arg = argv[i];
+        size_t len = strlen(arg);
+        if(len > 1 && arg[0] == '-') {
+            for(int c=1;c<len;c++) {
+                switch(arg[c]) {
+                    default: {
+                        fprintf(stderr, "Unknown option -%c\n\n", arg[c]);
+                        cmd_args_free(a);
+                        return NULL;
+                    }
+                    case 'c': {
+                        cmd_map_put(a->options, "conflict", NOARG);
+                        break;
+                    }
+                    case 'l': {
+                        cmd_map_put(a->options, "lock", NOARG);
+                        break;
+                    }
+                    case 'd': {
+                        cmd_map_put(a->options, "nolock", NOARG);
+                        break;
+                    }
+                    case 'r': {
+                        cmd_map_put(a->options, "remove", NOARG);
+                        break;
+                    }
+                    case 'v': {
+                        cmd_map_put(a->options, "verbose", NOARG);
+                        break;
+                    }
+                    case 's': {
+                        option = "syncdir";
+                        optchar = 's';
+                        break;
+                    }
+                    case 't': {
+                        option = "tags";
+                        optchar = 't';
+                        break;
+                    }
+                    case 'V': {
+                        option = "version";
+                        optchar = 'V';
+                        break;
+                    }
+                    case 'R': {
+                        cmd_map_put(a->options, "restore-removed", NOARG);
+                        break;
+                    }
+                    case 'M': {
+                        cmd_map_put(a->options, "restore-modified", NOARG);
+                        break;
+                    }
+                    case 'S': {
+                        cmd_map_put(a->options, "snapshot", NOARG);
+                    }
+                }
+            }
+        } else if(option) {
+            cmd_map_put(a->options, option, arg);
+            option = NULL;
+        } else {
+            a->argv[a->argc++] = arg;
+        }
+    }
+    
+    return a;
+}
+
+char* cmd_getoption(CmdArgs *arg, const char *name) {
+    return cxMapGet(arg->options, cx_hash_key_str(name));
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dav/sopt.h	Sat Nov 08 23:06:11 2025 +0100
@@ -0,0 +1,50 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2018 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef OPTPARSER_H
+#define	OPTPARSER_H
+
+#include <cx/map.h>
+#include "opt.h"
+
+#ifdef	__cplusplus
+extern "C" {
+#endif
+
+typedef void* ArgBool;
+    
+void cmd_args_free(CmdArgs *args);
+CmdArgs* cmd_parse_args(int argc, char **argv);
+char* cmd_getoption(CmdArgs *arg, const char *name);
+
+#ifdef	__cplusplus
+}
+#endif
+
+#endif	/* OPTPARSER_H */
+
--- a/dav/sync.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/dav/sync.c	Sat Nov 08 23:06:11 2025 +0100
@@ -39,7 +39,6 @@
 #include <cx/list.h>
 #include <cx/hash_map.h>
 #include <cx/printf.h>
-#include <cx/streams.h>
 
 #ifndef _WIN32
 // unix includes
@@ -59,16 +58,17 @@
 
 #include <libidav/session.h>
 
+#include <cx/streams.h>
+
 #include "sync.h"
 
 #include "config.h"
-#include "optparser.h"
+#include "sopt.h"
 #include "error.h"
 #include "assistant.h"
 #include "libxattr.h"
 #include "tags.h"
 #include "connect.h"
-#include "syncdir.h"
 
 #include "system.h"
 
@@ -202,19 +202,24 @@
     return s ? strdup(s) : NULL;
 }
 
-
-// TODO: remove, ucx supports creating iterators for NULL maps
+static void nullfree(void *p) {
+    if(p) {
+        free(p);
+    }
+}
+
+
+
 static CxMapIterator mapIteratorValues(CxMap *map) {
     return cxMapIteratorValues(map ? map : cxEmptyMap);
 }
-// TODO: remove
+
 static CxIterator listIterator(CxList *list) {
     return cxListIterator(list ? list : cxEmptyList);
 }
 
 typedef void*(*clonefunc)(void *elm, void *userdata);
 
-// TODO: remove when ucx has a clone func
 static CxMap* mapClone(const CxAllocator *a, CxMap *map, clonefunc clone, void *userdata) {
     CxMap *newmap = cxHashMapCreate(
             a,
@@ -237,6 +242,45 @@
     return newmap;
 }
 
+int dav_sync_main(int argc, char **argv);
+
+#ifdef _WIN32
+static char* wchar2utf8(const wchar_t *wstr, size_t wlen) {
+    size_t maxlen = wlen * 4;
+    char *ret = malloc(maxlen + 1);
+    int ret_len = WideCharToMultiByte(
+        CP_UTF8,
+        0,
+        wstr,
+        wlen,
+        ret,
+        maxlen,
+        NULL,
+        NULL);
+    ret[ret_len] = 0;
+    return ret;
+}
+
+int wmain(int argc, wchar_t **argv) {
+    char **argv_utf8 = calloc(argc, sizeof(char*));
+    for(int i=0;i<argc;i++) {
+        argv_utf8[i] = wchar2utf8(argv[i], wcslen(argv[i]));
+    }
+    
+    int ret = dav_sync_main(argc, argv_utf8);
+    
+    for(int i=0;i<argc;i++) {
+        free(argv_utf8[i]);
+    }
+    free(argv_utf8);
+    
+    return ret;
+}
+#else
+int main(int argc, char **argv) {
+    return dav_sync_main(argc, argv);
+}
+#endif
 
 int dav_sync_main(int argc, char **argv) {
     orig_argc = argc;
@@ -244,14 +288,14 @@
     
     if(argc < 2) {
         fprintf(stderr, "Missing command\n");
-        dav_sync_print_usage(argv[0]);
+        print_usage(argv[0]);
         return -1;
     }
     
     char *cmd = argv[1];
-    CmdArgs *args = cmd_parse_sync_args(argc - 2, argv + 2);
+    CmdArgs *args = cmd_parse_args(argc - 2, argv + 2);
     if(!args) {
-        dav_sync_print_usage(argv[0]);
+        print_usage(argv[0]);
         return -1;
     }
     int ret = EXIT_FAILURE;
@@ -344,7 +388,7 @@
                 || !strcmp(cmd, "check-repositories")) {
             ret = cmd_check_repositories(args);
         } else {
-            dav_sync_print_usage(argv[0]);
+            print_usage(argv[0]);
         }
     }
     
@@ -363,7 +407,7 @@
     return ret;
 }
 
-void dav_sync_print_usage(char *cmd) {
+void print_usage(char *cmd) {
     fprintf(stderr, "Usage: %s command [options] arguments...\n\n", cmd);
     
     fprintf(stderr, "Commands:\n");
@@ -448,6 +492,14 @@
 
 #endif
 
+static char* create_local_path(SyncDirectory *dir, const char *path) {
+    char *local_path = util_concat_path(dir->path, path);
+    size_t local_path_len = strlen(local_path);
+    if(local_path[local_path_len-1] == '/') {
+        local_path[local_path_len-1] = '\0';
+    }
+    return local_path;
+}
 
 static int res_matches_filter(Filter *filter, char *res_path) {
     // include/exclude filter
@@ -631,7 +683,6 @@
     return 1;
 }
 
-// TODO: remove: use dav_resource_map
 void res2map(DavResource *root, CxMap *map) {
     CxList *stack = cxLinkedListCreateSimple(CX_STORE_POINTERS);
     cxListInsert(stack, 0, root->children);
@@ -651,6 +702,18 @@
     cxListFree(stack);
 }
 
+static CxHashKey resource_path_key(DavResource *res) {
+    CxHashKey key = { NULL, 0, 0 };
+    if(res && res->path) {
+        cxstring res_path = cx_str(res->path);
+        if(res_path.length > 0 && res_path.ptr[res_path.length-1] == '/') {
+            res_path.length--;
+        }
+        key = cx_hash_key(res_path.ptr, res_path.length);
+    }
+    return key;
+}
+
 int cmd_pull(CmdArgs *a, DavBool incoming) {
     if(a->argc != 1) {
         fprintf(stderr, "Too %s arguments\n", a->argc < 1 ? "few" : "many");
@@ -715,7 +778,7 @@
     DavBool locked = FALSE;
     DavResource *root = dav_resource_new(sn, "/");
     root->iscollection = TRUE;
-    if((dir->lockpull || cmd_getoption(a, "lock")) && !cmd_getoption(a, "nolock")) {
+    if((dir->lockpush || cmd_getoption(a, "lock")) && !cmd_getoption(a, "nolock")) {
         if(dav_lock_t(root, dir->lock_timeout)) {
             log_resource_error(sn, "/");
             dav_session_destroy(sn);
@@ -863,7 +926,7 @@
             // remove every server resource from dbres
             // all remaining elements are the resources that are removed
             // on the server
-            cxMapRemove(dbres, dav_resource_path_key(res));
+            cxMapRemove(dbres, resource_path_key(res));
             
             if(!dav_get_property_ns(res, DAV_NS, "split") && res->children) {
                 cxListInsert(stack, 0, res->children);
@@ -912,7 +975,7 @@
     if(SYNC_HASHING(dir)) {
         // check for moved/copied files
         SYS_STAT s;
-        CxIterator mut_iter = cxListMutIterator(res_new);
+        CxIterator mut_iter = cxListIterator(res_new);
         cx_foreach(DavResource *, res, mut_iter) {
             if(dav_get_property_ns(res, DAV_PROPS_NS, "link")) {
                 continue;
@@ -1011,11 +1074,11 @@
             break;
         }
         
-        LocalResource *local = cxMapGet(db->resources, dav_resource_path_key(res));
+        LocalResource *local = cxMapGet(db->resources, resource_path_key(res));
         if(local) {
             log_printf("update: %s\n", res->path);
             char *res_path = resource_local_path(res);
-            char *local_path = syncdir_create_local_path(dir, res->path);
+            char *local_path = create_local_path(dir, res->path);
             free(res_path);
             if(sync_store_metadata(dir, local_path, local, res)) {
                 fprintf(stderr, "Metadata update failed: %s\n", res->path);
@@ -1113,6 +1176,237 @@
     return ret;
 }
 
+RemoteChangeType resource_get_remote_change(
+        CmdArgs *a,
+        DavResource *res,
+        SyncDirectory *dir,
+        SyncDatabase *db)
+{
+    DavBool update_db = FALSE;
+    
+    char *etag = dav_get_string_property(res, "D:getetag");
+    if(!etag) {
+        fprintf(stderr, "Error: resource %s has no etag\n", res->path);
+        return REMOTE_NO_CHANGE;
+    }
+    char *hash = sync_get_content_hash(res);
+      
+    DavBool issplit = dav_get_property(res, "idav:split") ? TRUE : FALSE;
+    if(issplit) {
+        util_remove_trailing_pathseparator(res->path);
+    }
+    DavBool iscollection = res->iscollection && !issplit;
+    
+    RemoteChangeType type = cmd_getoption(a, "conflict") ?
+            REMOTE_CHANGE_MODIFIED : REMOTE_CHANGE_CONFLICT_LOCAL_MODIFIED;
+    
+    LocalResource *local = cxMapGet(db->resources, resource_path_key(res));
+    char *local_path = create_local_path(dir, res->path);
+    
+    char *link = SYNC_SYMLINK(dir) ? 
+            dav_get_string_property_ns(res, DAV_PROPS_NS, "link") : NULL;
+    
+    SYS_STAT s;
+    DavBool exists = 1;
+    if(sys_stat(local_path, &s)) {
+        if(errno != ENOENT) {
+            fprintf(stderr, "Cannot stat file: %s\n", local_path);
+            free(local_path);
+            return REMOTE_NO_CHANGE;
+        }
+        exists = 0;
+    }
+    
+    RemoteChangeType ret = REMOTE_NO_CHANGE;
+    if(iscollection) {
+        if(!exists) {
+            ret = REMOTE_CHANGE_MKDIR;
+        } else if(local && S_ISDIR(s.st_mode)) {
+            local->isdirectory = 1; // make sure isdirectory is set
+        } else {
+            // set change to REMOTE_CHANGE_MKDIR, which will fail later
+            ret = REMOTE_CHANGE_MKDIR;
+        }
+    } else if(local) {
+        DavBool nochange = FALSE;
+        if(SYNC_SYMLINK(dir) && nullstrcmp(link, local->link_target)) {
+            ret = REMOTE_CHANGE_LINK;
+            nochange = TRUE;
+            
+            if(local->link_target) {
+                LocalResource *local2 = local_resource_new(dir, db, local->path);
+                if(type == REMOTE_CHANGE_CONFLICT_LOCAL_MODIFIED && nullstrcmp(local->link_target, local2->link_target)) {
+                    ret = REMOTE_CHANGE_CONFLICT_LOCAL_MODIFIED;
+                }            
+                local_resource_free(local2);
+
+                if(!nullstrcmp(link, local->link_target)) {
+                    ret = REMOTE_NO_CHANGE;
+                    update_db = TRUE;
+                }
+            } 
+        } else if(issplit && local->hash && hash) {
+            if(!strcmp(local->hash, hash)) {
+                // resource is already up-to-date on the client
+                nochange = TRUE;
+            }
+        } else if(local->etag) {
+            cxstring e = cx_str(etag);
+            if(cx_strprefix(e, CX_STR("W/"))) {
+                e = cx_strsubs(e, 2);
+            }
+            if(!strcmp(e.ptr, local->etag)) {
+                // resource is already up-to-date on the client
+                nochange = TRUE;
+            }
+        }
+        
+        if(!nochange) {
+            if(!(exists && s.st_mtime != local->last_modified)) {
+                type = REMOTE_CHANGE_MODIFIED;
+            }
+            ret = type;
+        }
+    } else if(link) {
+        // new file is a link
+        ret = REMOTE_CHANGE_LINK;
+        
+        if(exists && type == REMOTE_CHANGE_CONFLICT_LOCAL_MODIFIED) {
+            // a file with the same name already exists
+            // if it is a link, compare the targets
+            LocalResource *local2 = local_resource_new(dir, db, res->path);
+            if(local2) {
+                if(local2->link_target) {
+                    if(strcmp(link, local2->link_target)) {
+                        ret = REMOTE_CHANGE_CONFLICT_LOCAL_MODIFIED;
+                    }
+                } else {
+                    ret = REMOTE_CHANGE_CONFLICT_LOCAL_MODIFIED;
+                }
+                
+                local_resource_free(local2);
+            }
+        }
+        
+    } else if(exists) {
+        ret = type;
+    } else {
+        ret = REMOTE_CHANGE_NEW;
+    }
+     
+    // if hashing is enabled we can compare the hash of the remote file
+    // with the local file to test if a file is really modified
+    char *update_hash = NULL;
+    if (!iscollection &&
+        !link &&
+        (ret == REMOTE_CHANGE_MODIFIED || ret == REMOTE_CHANGE_CONFLICT_LOCAL_MODIFIED) &&
+        exists &&
+        hash &&
+        !dir->pull_skip_hashing)
+    {
+        // because rehashing a file is slow, there is a config element for
+        // disabling this (pull-skip-hashing)
+        char *local_hash = util_file_hash(local_path);
+        if(local_hash) {
+            if(!strcmp(hash, local_hash)) {
+                ret = REMOTE_NO_CHANGE;
+                update_db = TRUE;
+                update_hash = local_hash;
+                
+                // if local already exists, update the hash here
+                // because it is possible that there are metadata updates
+                // and in this case the db will updated later and needs
+                // the current hash
+                if(local) {
+                    if(local->hash) {
+                        free(local->hash);
+                    }
+                    local->hash = local_hash;
+                }
+            } else {
+                free(local_hash);
+            }
+        }
+    }
+    
+    // if a file is not modified, check if the metadata has changed
+    while(ret == REMOTE_NO_CHANGE && local) {
+        // check if tags have changed
+        if(dir->tagconfig) {
+            DavXmlNode *tagsprop = dav_get_property_ns(res, DAV_PROPS_NS, "tags");
+            CxList *remote_tags = NULL;
+            if(tagsprop) {
+                remote_tags = parse_dav_xml_taglist(tagsprop);
+            }
+            char *remote_hash = create_tags_hash(remote_tags);
+            if(nullstrcmp(remote_hash, local->remote_tags_hash)) {
+                ret = REMOTE_CHANGE_METADATA;
+            }
+            if(remote_hash) {
+                free(remote_hash);
+            }
+            free_taglist(remote_tags);
+            
+            if(ret == REMOTE_CHANGE_METADATA) {
+                break;
+            }
+        }
+        
+        // check if extended attributes have changed
+        if((dir->metadata & FINFO_XATTR) == FINFO_XATTR) {
+            DavXmlNode *xattr = dav_get_property_ns(res, DAV_PROPS_NS, "xattributes");
+            char *xattr_hash = get_xattr_hash(xattr);
+            if(nullstrcmp(xattr_hash, local->xattr_hash)) {
+                ret = REMOTE_CHANGE_METADATA;
+                break;
+            }
+        } 
+        
+        // check if finfo has changed
+        DavXmlNode *finfo = dav_get_property_ns(res, DAV_PROPS_NS, "finfo");
+        if((dir->metadata & FINFO_MODE) == FINFO_MODE) {
+            FileInfo f;
+            finfo_get_values(finfo, &f);
+            if(f.mode_set && f.mode != local->mode) {
+                ret = REMOTE_CHANGE_METADATA;
+                break;
+            }
+        }
+        
+        break;
+    }
+    
+    // if update_db is set, a file was modified on the server, but we already
+    // have the file content, but we need to update the db
+    if(ret == REMOTE_NO_CHANGE && update_db) {
+        if(!local) {
+            local = calloc(1, sizeof(LocalResource));
+            local->path = strdup(res->path);
+            
+            cxMapPut(db->resources, cx_hash_key_str(local->path), local);
+        }
+        
+        // update local res
+        SYS_STAT statdata;
+        if(!sys_stat(local_path, &statdata)) {
+            sync_set_metadata_from_stat(local, &statdata);
+        } else {
+            fprintf(stderr, "stat failed for file: %s : %s", local_path, strerror(errno));
+        }
+        local_resource_set_etag(local, etag);
+        if(!local->hash) {
+            local->hash = update_hash;
+        } // else: hash already updated
+        if(link) {
+            nullfree(local->link_target);
+            local->link_target = link;
+        }
+    }
+        
+    free(local_path);
+    return ret;
+}
+
 void sync_set_metadata_from_stat(LocalResource *local, SYS_STAT *s) {
     local->last_modified = s->st_mtime;
     local->mode = s->st_mode & 07777;
@@ -1351,10 +1645,10 @@
     char *local_path;
     if(link) {
         char *res_path = resource_local_path(res);
-        local_path = syncdir_create_local_path(dir, res_path);
+        local_path = create_local_path(dir, res_path);
         free(res_path);
     } else {
-        local_path = syncdir_create_local_path(dir, path);
+        local_path = create_local_path(dir, path);
     }
     
     char *etag = dav_get_string_property(res, "D:getetag");
@@ -1539,7 +1833,7 @@
     
     // TODO: use resource_local_path return value (necessary for creating links on windows)
     //char *res_path = resource_local_path(res);
-    char *local_path = syncdir_create_local_path(dir, res->path);
+    char *local_path = create_local_path(dir, res->path);
     //free(res_path);
      
     log_printf("get: %s\n", res->path);
@@ -1598,7 +1892,7 @@
 }
 
 int sync_remove_local_resource(SyncDirectory *dir, LocalResource *res) {
-    char *local_path = syncdir_create_local_path(dir, res->path);
+    char *local_path = create_local_path(dir, res->path);
     SYS_STAT s;
     if(sys_stat(local_path, &s)) {
         free(local_path);
@@ -1630,7 +1924,7 @@
 
 int sync_remove_local_directory(SyncDirectory *dir, LocalResource *res) {
     int ret = 0;
-    char *local_path = syncdir_create_local_path(dir, res->path);
+    char *local_path = create_local_path(dir, res->path);
     
     log_printf("delete: %s\n", res->path);
     if(rmdir(local_path)) {
@@ -1649,7 +1943,7 @@
 }
 
 void rename_conflict_file(SyncDirectory *dir, SyncDatabase *db, char *path, DavBool copy) {
-    char *local_path = syncdir_create_local_path(dir, path);
+    char *local_path = create_local_path(dir, path);
     char *parent = util_parent_path(local_path);
     
     renamefunc fn = copy ? copy_file : sys_rename;
@@ -1970,7 +2264,7 @@
     if(SYNC_STORE_HASH(dir)) {
         // calculate hashes of all new files and check if a file
         // was moved or is a copy
-        CxIterator mut_iter = cxListMutIterator(ls_new);
+        CxIterator mut_iter = cxListIterator(ls_new);
         cx_foreach(LocalResource *, local, mut_iter) {
             if(local->isdirectory || local->link_target) {
                 continue;
@@ -2136,9 +2430,9 @@
             if(dav_exists(res)) {
                 log_printf("conflict: %s\n", local->path);
                 local->last_modified = 0;
-                free(local->etag);
+                nullfree(local->etag);
                 local->etag = NULL;
-                free(local->hash);
+                nullfree(local->hash);
                 local->hash = NULL;
                 local->skipped = TRUE;
                 sync_conflict++;
@@ -2214,9 +2508,9 @@
                 } else if(cdt && changed) {
                     log_printf("conflict: %s\n", local_res->path);
                     local_res->last_modified = 0;
-                    free(local_res->etag);
+                    nullfree(local_res->etag);
                     local_res->etag = NULL;
-                    free(local_res->hash);
+                    nullfree(local_res->hash);
                     local_res->hash = NULL;
                     local_res->skipped = TRUE;
                     sync_conflict++;
@@ -2455,7 +2749,7 @@
             }
         }
         
-        char *file_path = syncdir_create_local_path(dir, resource->path);
+        char *file_path = create_local_path(dir, resource->path);
         SYS_STAT s;
         if(sys_stat(file_path, &s)) {
             if(errno == ENOENT) {
@@ -2568,7 +2862,7 @@
             // download the resource
             if(!sync_shutdown) {
                 if(resource->isdirectory) {
-                    char *local_path = syncdir_create_local_path(dir, res->path);
+                    char *local_path = create_local_path(dir, res->path);
                     if(sys_mkdir(local_path) && errno != EEXIST) {
                         log_error(
                                 "Cannot create directory %s: %s",
@@ -2583,7 +2877,7 @@
                         LocalResource *lr = cxMapGet(db->resources, cx_hash_key_str(res->path));
                         if(lr) {
                             lr->last_modified = 0;
-                            free(lr->hash);
+                            nullfree(lr->hash);
                             lr->hash = NULL;
                         } // else should not happen
                     }
@@ -2756,7 +3050,7 @@
         
         char *p = cxListAt(stack, 0);
         cxListRemove(stack, 0);
-        char *local_path = syncdir_create_local_path(dir, p);
+        char *local_path = create_local_path(dir, p);
         SYS_DIR local_dir = sys_opendir(local_path);
         
         if(!local_dir) {
@@ -2796,7 +3090,7 @@
 
 
 LocalResource* local_resource_new(SyncDirectory *dir, SyncDatabase *db, char *path) {
-    char *file_path = syncdir_create_local_path(dir, path);
+    char *file_path = create_local_path(dir, path);
     SYS_STAT s;
     if(sys_lstat(file_path, &s)) {
         log_error("Cannot stat file %s: %s\n", file_path, strerror(errno));
@@ -3017,7 +3311,7 @@
         
         // check if xattr have changed
         if((dir->metadata & FINFO_XATTR) == FINFO_XATTR) {
-            char *path = syncdir_create_local_path(dir, local_resource_path(db_res));
+            char *path = create_local_path(dir, local_resource_path(db_res));
             XAttributes *xattr = file_get_attributes(path, (xattr_filter_func)xattr_filter, dir);
             // test if xattr are added, removed or changed
             if((db_res->xattr_hash && !xattr) ||
@@ -3152,7 +3446,7 @@
 int local_resource_load_metadata(SyncDirectory *dir, LocalResource *res) {
     // currently only xattr needed
     if((dir->metadata & FINFO_XATTR) == FINFO_XATTR) {
-        char *path = syncdir_create_local_path(dir, local_resource_path(res));
+        char *path = create_local_path(dir, local_resource_path(res));
         XAttributes *xattr = file_get_attributes(path, (xattr_filter_func)xattr_filter, dir);
         res->xattr = xattr;
         free(path);
@@ -3485,7 +3779,7 @@
     }
     
     if(!store_tags) {
-        free(local->remote_tags_hash);
+        nullfree(local->remote_tags_hash);
         local->remote_tags_hash = remote_hash;
         return 0;
     }
@@ -3576,7 +3870,7 @@
     CxBuffer *buf = NULL;
     if(dir->tagconfig->store == TAG_STORE_XATTR) {
         ssize_t tag_length = 0;
-        char *local_path = syncdir_create_local_path(dir, local_resource_path(res));
+        char *local_path = create_local_path(dir, local_resource_path(res));
         char* tag_data = xattr_get(
                 local_path,
                 dir->tagconfig->xattr_name,
@@ -4104,7 +4398,7 @@
         LocalResource *local,
         int *counter)
 {
-    char *local_path = syncdir_create_local_path(dir, local_resource_path(local));
+    char *local_path = create_local_path(dir, local_resource_path(local));
     
     SYS_STAT s;
     if(sys_stat(local_path, &s)) {
@@ -4275,7 +4569,7 @@
         DavBool copy,
         int *counter)
 {
-    char *local_path = syncdir_create_local_path(dir, local->path);
+    char *local_path = create_local_path(dir, local->path);
     
     SYS_STAT s;
     if(sys_stat(local_path, &s)) {
@@ -4458,8 +4752,8 @@
                         free_taglist(tags);
                         tags = new_tags;
                         
-                        free(tags_hash);
-                        free(new_remote_hash);
+                        nullfree(tags_hash);
+                        nullfree(new_remote_hash);
                         tags_hash = create_tags_hash(tags);
                         new_remote_hash = nullstrdup(tags_hash);
                         
@@ -4467,7 +4761,7 @@
                     }
                 }
             }
-            free(remote_hash);
+            nullfree(remote_hash);
             
             if(dir->tagconfig->local_format == TAG_FORMAT_CSV) {
                 // csv tag lists don't have colors, so we have to add
@@ -4552,7 +4846,7 @@
     
     CxMapIterator i = cxMapIteratorValues(db->conflict);
     cx_foreach(LocalResource *, res, i) {
-        char *path = syncdir_create_local_path(dir, res->path);
+        char *path = create_local_path(dir, res->path);
         SYS_STAT s;
         if(sys_stat(path, &s)) {
             if(errno == ENOENT) {
@@ -4675,7 +4969,7 @@
     CxMapIterator i = cxMapIteratorValues(db->conflict);
     cx_foreach(LocalResource*, res, i) {
         log_printf("delete: %s\n", res->path);
-        char *path = syncdir_create_local_path(dir, res->path);
+        char *path = create_local_path(dir, res->path);
         if(sys_unlink(path)) {
             if(errno != ENOENT) {
                 log_error("unlink: %s", strerror(errno));
--- a/dav/sync.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/dav/sync.h	Sat Nov 08 23:06:11 2025 +0100
@@ -36,7 +36,7 @@
 
 #include "scfg.h"
 #include "config.h"
-#include "optparser.h"
+#include "sopt.h"
 #include "system.h"
 
 #include "version.h"
@@ -92,9 +92,7 @@
     RemoteChangeType type;
 } RemoteChange;
 
-void dav_sync_print_usage(char *cmd);
-
-int dav_sync_main(int argc, char **argv);
+void print_usage(char *cmd);
 
 #ifndef _WIN32
 pthread_t start_sighandler(pthread_mutex_t *mutex) ;
--- a/dav/sync_main.c	Tue Oct 14 21:02:26 2025 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,81 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2025 Olaf Wintermann. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#include "sync_main.h"
-
-#include <stdlib.h>
-
-
-#include "system.h"
-
-#ifndef _WIN32
-// unix includes
-#include <unistd.h>
-#include <pthread.h>
-#else
-//windows includes
-
-#endif
-
-#ifdef _WIN32
-static char* wchar2utf8(const wchar_t *wstr, size_t wlen) {
-    size_t maxlen = wlen * 4;
-    char *ret = malloc(maxlen + 1);
-    int ret_len = WideCharToMultiByte(
-        CP_UTF8,
-        0,
-        wstr,
-        wlen,
-        ret,
-        maxlen,
-        NULL,
-        NULL);
-    ret[ret_len] = 0;
-    return ret;
-}
-
-int wmain(int argc, wchar_t **argv) {
-    char **argv_utf8 = calloc(argc, sizeof(char*));
-    for(int i=0;i<argc;i++) {
-        argv_utf8[i] = wchar2utf8(argv[i], wcslen(argv[i]));
-    }
-    
-    int ret = dav_sync_main(argc, argv_utf8);
-    
-    for(int i=0;i<argc;i++) {
-        free(argv_utf8[i]);
-    }
-    free(argv_utf8);
-    
-    return ret;
-}
-#else
-int main(int argc, char **argv) {
-    return dav_sync_main(argc, argv);
-}
-#endif
--- a/dav/sync_main.h	Tue Oct 14 21:02:26 2025 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,46 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2025 Olaf Wintermann. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#ifndef SYNC_MAIN_H
-#define SYNC_MAIN_H
-
-#include "sync.h"
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-
-
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /* SYNC_MAIN_H */
-
--- a/dav/syncdir.c	Tue Oct 14 21:02:26 2025 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,41 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2025 Olaf Wintermann. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#include "syncdir.h"
-
-#include <string.h>
-#include <libidav/utils.h>
-
-char* syncdir_create_local_path(SyncDirectory *dir, const char *path) {
-    char *local_path = util_concat_path(dir->path, path);
-    size_t local_path_len = strlen(local_path);
-    if(local_path[local_path_len-1] == '/') {
-        local_path[local_path_len-1] = '\0';
-    }
-    return local_path;
-}
--- a/dav/syncdir.h	Tue Oct 14 21:02:26 2025 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,46 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2025 Olaf Wintermann. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#ifndef SYNCDIR_H
-#define SYNCDIR_H
-
-#include "sync.h"
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-char* syncdir_create_local_path(SyncDirectory *dir, const char *path);
-
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /* SYNCDIR_H */
-
--- a/dav/xattrtool.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/dav/xattrtool.c	Sat Nov 08 23:06:11 2025 +0100
@@ -46,7 +46,7 @@
 int attrtool_set(int argc, char **argv);
 int attrtool_rm(int argc, char **argv);
 
-void dav_sync_print_usage(char *cmd) {
+void print_usage(char *cmd) {
     fprintf(stderr, "usage %s:\n", cmd);
     fprintf(stderr, "   list <file>\n");
     fprintf(stderr, "   listvalues <file>\n");
@@ -97,7 +97,7 @@
 
 int xattr_tool_main(int argc, char **argv) {
     if(argc < 3) {
-        dav_sync_print_usage(argv[0]);
+        print_usage(argv[0]);
         return 1;
     }
     
@@ -113,7 +113,7 @@
         return attrtool_rm(argc, argv);
     } else {
         fprintf(stderr, "Unknown command\n");
-        dav_sync_print_usage(argv[0]);
+        print_usage(argv[0]);
     }
     
     return 1;
@@ -157,7 +157,7 @@
 int attrtool_get(int argc, char **argv, int raw) {
     if(argc < 4) {
         fprintf(stderr, "Too few arguments\n");
-        dav_sync_print_usage(argv[1]);
+        print_usage(argv[1]);
         return 1;
     }
     
@@ -185,7 +185,7 @@
 int attrtool_set(int argc, char **argv) {
     if(argc < 4) {
         fprintf(stderr, "Too few arguments\n");
-        dav_sync_print_usage(argv[1]);
+        print_usage(argv[1]);
         return 1;
     }
     
@@ -206,7 +206,7 @@
 int attrtool_rm(int argc, char **argv) {
     if(argc < 4) {
         fprintf(stderr, "Too few arguments\n");
-        dav_sync_print_usage(argv[1]);
+        print_usage(argv[1]);
         return 1;
     }
     
--- a/libidav/pwdstore.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/libidav/pwdstore.c	Sat Nov 08 23:06:11 2025 +0100
@@ -34,8 +34,9 @@
 #include <stdlib.h>
 #include <string.h>
 
+#include <cx/streams.h> 
+
 #include <cx/hash_map.h>
-#include <cx/streams.h>
 
 #ifdef _WIN32
 #include <winsock.h>
@@ -263,7 +264,7 @@
 }
 
 static void remove_list_entries(PwdStore *s, const char *id) {
-    CxIterator i = cxListMutIterator(s->locations);
+    CxIterator i = cxListIterator(s->locations);
     cx_foreach(PwdIndexEntry*, ie, i) {
         if(!strcmp(ie->id, id)) {
             cxIteratorFlagRemoval(i);
@@ -271,7 +272,7 @@
             break;
         }
     }
-    i = cxListMutIterator(s->noloc);
+    i = cxListIterator(s->noloc);
     cx_foreach(PwdIndexEntry*, ie, i) {
         if(!strcmp(ie->id, id)) {
             cxIteratorFlagRemoval(i);
--- a/libidav/resource.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/libidav/resource.c	Sat Nov 08 23:06:11 2025 +0100
@@ -280,47 +280,6 @@
     return resource->href;
 }
 
-CxHashKey dav_resource_path_key(DavResource *res) {
-    CxHashKey key = { NULL, 0, 0 };
-    if(res && res->path) {
-        cxstring res_path = cx_str(res->path);
-        if(res_path.length > 0 && res_path.ptr[res_path.length-1] == '/') {
-            res_path.length--;
-        }
-        key = cx_hash_key(res_path.ptr, res_path.length);
-    }
-    return key;
-}
-
-CxTreeIterator dav_resource_iterator(DavResource *res) {
-    return cx_tree_iterator(res, FALSE, offsetof(DavResource, children), offsetof(DavResource, next));
-}
-
-CxTreeVisitor dav_resource_visitor(DavResource *res) {
-    return cx_tree_visitor(res, offsetof(DavResource, children), offsetof(DavResource, next));
-}
-
-CxMap* dav_resource_map(DavResource *res) {
-    CxMap *map = cxHashMapCreate(res->session->mp->allocator, CX_STORE_POINTERS, 16);
-    if(!map) {
-        return NULL;
-    }
-    CxTreeIterator i = dav_resource_iterator(res);
-    int error = 0;
-    cx_foreach(DavResource *, res, i) {
-        if(cxMapPut(map, res->path, res)) {
-            error = 1;
-        }
-    }
-    if(!error) {
-        cxMapRehash(map);
-        return map;
-    } else {
-        cxMapFree(map);
-        return NULL;
-    }
-}
-
 void resource_add_prop(DavResource *res, const char *ns, const char *name, DavXmlNode *val) {
     DavSession *sn = res->session;
     
@@ -1639,7 +1598,7 @@
             property->value = n->children ? dav_convert_xml(sn, n->children) : NULL;
             
             cxmutstr propkey = dav_property_key(property->ns->name, property->name);
-            cxMapPut(map, cx_hash_key_cxstr(propkey), property);
+            cxMapPut(map, propkey, property);
             cx_strfree(&propkey);
         }
         n = n->next;
--- a/libidav/webdav.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/libidav/webdav.c	Sat Nov 08 23:06:11 2025 +0100
@@ -36,7 +36,6 @@
 #include "session.h"
 #include "methods.h"
 #include <cx/buffer.h>
-#include <cx/printf.h>
 #include <cx/linked_list.h>
 #include <cx/hash_map.h>
 #include <cx/compare.h>
--- a/libidav/webdav.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/libidav/webdav.h	Sat Nov 08 23:06:11 2025 +0100
@@ -36,7 +36,6 @@
 #include <cx/linked_list.h>
 #include <cx/string.h>
 #include <cx/buffer.h>
-#include <cx/tree.h>
 #include <curl/curl.h>
 #include <libxml/tree.h>
 
@@ -338,12 +337,6 @@
 
 char* dav_resource_get_href(DavResource *resource);
 
-CxHashKey dav_resource_path_key(DavResource *res);
-
-CxTreeIterator dav_resource_iterator(DavResource *res);
-CxTreeVisitor dav_resource_visitor(DavResource *res);
-CxMap* dav_resource_map(DavResource *res);
-
 DavResource* dav_create_child(DavResource *parent, char *name);
 int dav_delete(DavResource *res);
 int dav_create(DavResource *res);
--- a/test/Makefile	Tue Oct 14 21:02:26 2025 +0200
+++ b/test/Makefile	Sat Nov 08 23:06:11 2025 +0100
@@ -28,16 +28,11 @@
 
 include ../config.mk
 
-include ../dav/obj.mk
-
 TEST_SRC  = main.c
-TEST_SRC += helper.c
+TEST_SRC += test.c
 TEST_SRC += base64.c
 TEST_SRC += crypto.c
 TEST_SRC += utils.c
-TEST_SRC += test_resource.c
-
-TEST_SRC += test_finfo.c
 
 
 TEST_OBJ = $(TEST_SRC:%.c=../build/test/%$(OBJ_EXT))
@@ -46,8 +41,8 @@
 
 all: $(TEST_TARGET)
 
-$(TEST_TARGET): $(TEST_OBJ) $(DAV_OBJ) $(SYNC_OBJ) $(COMMON_OBJ) ../build/lib/libidav$(LIB_EXT)
-	$(CC) -o $(TEST_TARGET) $(TEST_OBJ) $(DAV_OBJ) $(SYNC_OBJ) $(COMMON_OBJ) \
+$(TEST_TARGET): $(TEST_OBJ) ../build/lib/libidav$(LIB_EXT)
+	$(CC) -o $(TEST_TARGET) $(TEST_OBJ) \
 		../build/lib/libidav$(LIB_EXT) ../build/lib/libucx$(LIB_EXT)  \
 		$(LDFLAGS) $(DAV_LDFLAGS)
 
--- a/test/base64.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/test/base64.c	Sat Nov 08 23:06:11 2025 +0100
@@ -35,18 +35,20 @@
 #include <cx/string.h>
 #include <libidav/utils.h>
 
-CX_TEST(test_util_base64decode) {
+UCX_TEST(test_util_base64decode) {
     char *s1 = util_base64decode("YWJj");
     char *s2 = util_base64decode("aGVsbG8gd29ybGQ=");
     char *s3 = util_base64decode("MA==");
     char *s4 = util_base64decode("MHh4MQ==");
     
-    CX_TEST_DO {
-        CX_TEST_ASSERT(!strcmp(s1, "abc"));
-        CX_TEST_ASSERT(!strcmp(s2, "hello world"));
-        CX_TEST_ASSERT(!strcmp(s3, "0"));
-        CX_TEST_ASSERT(!strcmp(s4, "0xx1"));
-    }
+    UCX_TEST_BEGIN;
+    
+    UCX_TEST_ASSERT(!strcmp(s1, "abc"), "s1 wrong");
+    UCX_TEST_ASSERT(!strcmp(s2, "hello world"), "s2 wrong");
+    UCX_TEST_ASSERT(!strcmp(s3, "0"), "s3 wrong");
+    UCX_TEST_ASSERT(!strcmp(s4, "0xx1"), "s4 wrong");
+    
+    UCX_TEST_END;
     
     free(s1);
     free(s2);
@@ -54,7 +56,7 @@
     free(s4);
 }
 
-CX_TEST(test_util_base64decode_len) {
+UCX_TEST(test_util_base64decode_len) {
     int len1, len2, len3, len4, len5, len6;
     char *s1 = util_base64decode_len("aGVsbG8=", &len1); // 5
     char *s2 = util_base64decode_len("MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5", &len2); // 30
@@ -63,25 +65,27 @@
     char *s5 = util_base64decode_len("YWJjLS0tLS0tM3g=", &len5); // 11
     char *s6 = util_base64decode_len("YWJjZGVmZy4uLjs7MGhlbGxv", &len6); // 18
     
-    CX_TEST_DO {
-        CX_TEST_ASSERT(!strcmp(s1, "hello"));
-        CX_TEST_ASSERT(len1 == 5);
-
-        CX_TEST_ASSERT(!strcmp(s2, "012345678901234567890123456789"));
-        CX_TEST_ASSERT(len2 == 30);
-
-        CX_TEST_ASSERT(!strcmp(s3, "."));
-        CX_TEST_ASSERT(len3 == 1);
-
-        CX_TEST_ASSERT(!strcmp(s4, "lol"));
-        CX_TEST_ASSERT(len4 == 3);
-
-        CX_TEST_ASSERT(!strcmp(s5, "abc------3x"));
-        CX_TEST_ASSERT(len5 == 11);
-
-        CX_TEST_ASSERT(!strcmp(s6, "abcdefg...;;0hello"));
-        CX_TEST_ASSERT(len6 == 18);
-    }
+    UCX_TEST_BEGIN;
+    
+    UCX_TEST_ASSERT(!strcmp(s1, "hello"), "s1 wrong");
+    UCX_TEST_ASSERT(len1 == 5, "len1 wrong");
+    
+    UCX_TEST_ASSERT(!strcmp(s2, "012345678901234567890123456789"), "s2 wrong");
+    UCX_TEST_ASSERT(len2 == 30, "len2 wrong");
+    
+    UCX_TEST_ASSERT(!strcmp(s3, "."), "s3 wrong");
+    UCX_TEST_ASSERT(len3 == 1, "len3 wrong");
+    
+    UCX_TEST_ASSERT(!strcmp(s4, "lol"), "s4 wrong");
+    UCX_TEST_ASSERT(len4 == 3, "len4 wrong");
+    
+    UCX_TEST_ASSERT(!strcmp(s5, "abc------3x"), "s5 wrong");
+    UCX_TEST_ASSERT(len5 == 11, "len5 wrong");
+    
+    UCX_TEST_ASSERT(!strcmp(s6, "abcdefg...;;0hello"), "s6 failed");
+    UCX_TEST_ASSERT(len6 == 18, "len6 wrong");
+    
+    UCX_TEST_END;
     
     free(s1);
     free(s2);
@@ -91,7 +95,7 @@
     free(s6);
 }
 
-CX_TEST(test_util_base64encode) {  
+UCX_TEST(test_util_base64encode) {  
     char *str1 = "hello world";
     char *str2 = "test string";
     char *str3 = "01234567890123456789012345678901234567890123456789";
@@ -123,19 +127,21 @@
     char *b9 = util_base64encode(str9, strlen(str9));
     char *b10 = util_base64encode(str10, strlen(str10));
     
-    CX_TEST_DO {
-        CX_TEST_ASSERT(!strcmp(b1, "aGVsbG8gd29ybGQ="));
-        CX_TEST_ASSERT(!strcmp(b2, "dGVzdCBzdHJpbmc="));
-        CX_TEST_ASSERT(!strcmp(b3, "MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODk="));
-        CX_TEST_ASSERT(!strcmp(b4, "AAwDMgJuPwE="));
-
-        CX_TEST_ASSERT(!strcmp(b5, "YQ=="));
-        CX_TEST_ASSERT(!strcmp(b6, "YWI="));
-        CX_TEST_ASSERT(!strcmp(b7, "YWJj"));
-        CX_TEST_ASSERT(!strcmp(b8, "YWJjZA=="));
-        CX_TEST_ASSERT(!strcmp(b9, "YWJjZGU="));
-        CX_TEST_ASSERT(!strcmp(b10, "YWJjZGVm"));
-    }
+    UCX_TEST_BEGIN;
+    
+    UCX_TEST_ASSERT(!strcmp(b1, "aGVsbG8gd29ybGQ="), "b1 failed");
+    UCX_TEST_ASSERT(!strcmp(b2, "dGVzdCBzdHJpbmc="), "b2 failed");
+    UCX_TEST_ASSERT(!strcmp(b3, "MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODk="), "b3 failed");
+    UCX_TEST_ASSERT(!strcmp(b4, "AAwDMgJuPwE="), "b4 failed");
+    
+    UCX_TEST_ASSERT(!strcmp(b5, "YQ=="), "b5 failed");
+    UCX_TEST_ASSERT(!strcmp(b6, "YWI="), "b6 failed");
+    UCX_TEST_ASSERT(!strcmp(b7, "YWJj"), "b7 failed");
+    UCX_TEST_ASSERT(!strcmp(b8, "YWJjZA=="), "b8 failed");
+    UCX_TEST_ASSERT(!strcmp(b9, "YWJjZGU="), "b9 failed");
+    UCX_TEST_ASSERT(!strcmp(b10, "YWJjZGVm"), "b10 failed");
+    
+    UCX_TEST_END;
     
     free(b1);
     free(b2);
--- a/test/base64.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/test/base64.h	Sat Nov 08 23:06:11 2025 +0100
@@ -29,15 +29,15 @@
 #ifndef BASE64_H
 #define BASE64_H
 
-#include <cx/test.h>
+#include "test.h"
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
-CX_TEST(test_util_base64decode);
-CX_TEST(test_util_base64decode_len);
-CX_TEST(test_util_base64encode);
+UCX_TEST(test_util_base64decode);
+UCX_TEST(test_util_base64decode_len);
+UCX_TEST(test_util_base64encode);
 
 
 #ifdef __cplusplus
--- a/test/crypto.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/test/crypto.c	Sat Nov 08 23:06:11 2025 +0100
@@ -191,138 +191,146 @@
     }
 }
 
-CX_TEST(test_util_decrypt_str_k) {
+UCX_TEST(test_util_decrypt_str_k) {
     create_keys();
     
     DavContext *ctx = dav_context_new();
     DavSession *sn = dav_session_new(ctx, "http://localhost/");
     
-    CX_TEST_DO {
-        for(int k=0;k<2;k++) {
-            for(int i=0;i<20;i++) {
-                char *c1;
-                char *c2;
-                if(k == 0) {
-                    c1 = cryptkey0aes128strings[i];
-                    c2 = cryptkey0aes256strings[i];
-                } else {
-                    c1 = cryptkey1aes128strings[i];
-                    c2 = cryptkey1aes256strings[i];
-                }
-
-                char *s128 = util_decrypt_str_k(sn, c1, &keys128[k]);
-                char *s256 = util_decrypt_str_k(sn, c2, &keys256[k]);
-
-                CX_TEST_ASSERT(!strcmp(s128, strings[i]));
-                CX_TEST_ASSERT(!strcmp(s256, strings[i]));
+    UCX_TEST_BEGIN;
+    
+    for(int k=0;k<2;k++) {
+        for(int i=0;i<20;i++) {
+            char *c1;
+            char *c2;
+            if(k == 0) {
+                c1 = cryptkey0aes128strings[i];
+                c2 = cryptkey0aes256strings[i];
+            } else {
+                c1 = cryptkey1aes128strings[i];
+                c2 = cryptkey1aes256strings[i];
             }
+            
+            char *s128 = util_decrypt_str_k(sn, c1, &keys128[k]);
+            char *s256 = util_decrypt_str_k(sn, c2, &keys256[k]);
+            
+            UCX_TEST_ASSERT(!strcmp(s128, strings[i]), "s128 wrong");
+            UCX_TEST_ASSERT(!strcmp(s256, strings[i]), "s256 wrong");
         }
     }
     
+    UCX_TEST_END;
+    
     dav_context_destroy(ctx);
 }
 
-CX_TEST(test_util_encrypt_str_k) {
+UCX_TEST(test_util_encrypt_str_k) {
     DavContext *ctx = dav_context_new();
     DavSession *sn = dav_session_new(ctx, "http://localhost/");
     
-    CX_TEST_DO {
-        for(int k=0;k<16;k++) {
-            for(int i=0;i<20;i++) {
-                char *aes128 = util_encrypt_str_k(sn, strings[i], &keys128[k]);
-                char *aes256 = util_encrypt_str_k(sn, strings[i], &keys256[k]);
-
-                char *dec1 = util_decrypt_str_k(sn, aes128, &keys128[k]);
-                char *dec2 = util_decrypt_str_k(sn, aes256, &keys256[k]);
-
-                CX_TEST_ASSERT(!strcmp(dec1, strings[i]));
-                CX_TEST_ASSERT(!strcmp(dec2, strings[i]));
-            }
+    UCX_TEST_BEGIN;
+    
+    for(int k=0;k<16;k++) {
+        for(int i=0;i<20;i++) {
+            char *aes128 = util_encrypt_str_k(sn, strings[i], &keys128[k]);
+            char *aes256 = util_encrypt_str_k(sn, strings[i], &keys256[k]);
+            
+            char *dec1 = util_decrypt_str_k(sn, aes128, &keys128[k]);
+            char *dec2 = util_decrypt_str_k(sn, aes256, &keys256[k]);
+            
+            UCX_TEST_ASSERT(!strcmp(dec1, strings[i]), "aes128 encrypt failed");
+            UCX_TEST_ASSERT(!strcmp(dec2, strings[i]), "aes256 encrypt failed");
         }
     }
+    
+    UCX_TEST_END;
 }
 
-CX_TEST(test_crypto_buffer) {
-    CX_TEST_DO {
-        for(int i=0;i<32;i++) {
-            DavKey *key = i < 16 ? &keys256[i] : &keys128[i%16];
-
-            for(int j=0;j<20;j++) {
-                CxBuffer *content = cxBufferCreate(NULL, 256, cxDefaultAllocator, CX_BUFFER_FREE_CONTENTS|CX_BUFFER_AUTO_EXTEND);
-                cxBufferPutString(content, strings[j]);
-                content->pos = 0;
-
-                CxBuffer *enc = aes_encrypt_buffer(content, key);
-                CX_TEST_ASSERT(enc->size >= content->size + 16);
-
-                char *base64 = util_base64encode(enc->space, enc->size);
-                size_t plainlen = 0;
-                char *plain = aes_decrypt(base64, &plainlen, key);
-
-                CX_TEST_ASSERT(plain);
-                CX_TEST_ASSERT(plainlen == content->size);
-                CX_TEST_ASSERT(!memcmp(plain, content->space, plainlen));
-
-                CxBuffer *dec = aes_decrypt_buffer(enc, key);
-                CX_TEST_ASSERT(dec->size == content->size);
-
-                CX_TEST_ASSERT(!memcmp(content->space, dec->space, dec->size));
-
-                cxBufferFree(content);
-                cxBufferFree(enc);
-                cxBufferFree(dec);
-                free(base64);
-                free(plain);
-            }
+UCX_TEST(test_crypto_buffer) {
+    UCX_TEST_BEGIN;
+    
+    for(int i=0;i<32;i++) {
+        DavKey *key = i < 16 ? &keys256[i] : &keys128[i%16];
+        
+        for(int j=0;j<20;j++) {
+            CxBuffer *content = cxBufferCreate(NULL, 256, cxDefaultAllocator, CX_BUFFER_FREE_CONTENTS|CX_BUFFER_AUTO_EXTEND);
+            cxBufferPutString(content, strings[j]);
+            content->pos = 0;
+            
+            CxBuffer *enc = aes_encrypt_buffer(content, key);
+            UCX_TEST_ASSERT(enc->size >= content->size + 16, "aes_encrypt_buffer failed");
+            
+            char *base64 = util_base64encode(enc->space, enc->size);
+            size_t plainlen = 0;
+            char *plain = aes_decrypt(base64, &plainlen, key);
+            
+            UCX_TEST_ASSERT(plain, "aes_decrypt failed");
+            UCX_TEST_ASSERT(plainlen == content->size, "aes_decrypt: wrong length");
+            UCX_TEST_ASSERT(!memcmp(plain, content->space, plainlen), "aes_decrypt: wrong content");
+            
+            CxBuffer *dec = aes_decrypt_buffer(enc, key);
+            UCX_TEST_ASSERT(dec->size == content->size, "aes_decrypt_buffer failed");
+            
+            UCX_TEST_ASSERT(!memcmp(content->space, dec->space, dec->size), "decrypted buffer has wrong content");
+            
+            cxBufferFree(content);
+            cxBufferFree(enc);
+            cxBufferFree(dec);
+            free(base64);
+            free(plain);
         }
     }
+    
+    UCX_TEST_END;
 }
 
-CX_TEST(test_crypto_stream) {
+UCX_TEST(test_crypto_stream) {
     CxBuffer *data = cxBufferCreate(NULL, 1024, cxDefaultAllocator, CX_BUFFER_FREE_CONTENTS|CX_BUFFER_AUTO_EXTEND);
     CxBuffer *cbuf = cxBufferCreate(NULL, 1024, cxDefaultAllocator, CX_BUFFER_FREE_CONTENTS|CX_BUFFER_AUTO_EXTEND);
     CxBuffer *pbuf = cxBufferCreate(NULL, 1024, cxDefaultAllocator, CX_BUFFER_FREE_CONTENTS|CX_BUFFER_AUTO_EXTEND);
     
-    CX_TEST_DO {
-        for(int i=0;i<32;i++) {
-            DavKey *key = i < 16 ? &keys256[i] : &keys128[i%16];
-            for(int j=0;j<20;j++) {
-                data->pos = 0;
-                data->size = 0;
-                size_t slen = strlen(strings[j]);
-                cxBufferWrite(strings[j], 1, slen, data);
-                cxBufferSeek(data, 0, SEEK_SET);
+    UCX_TEST_BEGIN;
+    
+    for(int i=0;i<32;i++) {
+        DavKey *key = i < 16 ? &keys256[i] : &keys128[i%16];
+        for(int j=0;j<20;j++) {
+            data->pos = 0;
+            data->size = 0;
+            size_t slen = strlen(strings[j]);
+            cxBufferWrite(strings[j], 1, slen, data);
+            cxBufferSeek(data, 0, SEEK_SET);
 
-                cbuf->pos = 0;
-                cbuf->size = 0;
-                pbuf->pos = 0;
-                pbuf->size = 0;
+            cbuf->pos = 0;
+            cbuf->size = 0;
+            pbuf->pos = 0;
+            pbuf->size = 0;
 
-                AESEncrypter *enc = aes_encrypter_new(key, data, (dav_read_func)cxBufferRead, NULL);
-                char buf[1024];
-                size_t r = 0;
-                while((r = aes_read(buf, 1, 1024, enc)) != 0) {
-                    cxBufferWrite(buf, 1, r, cbuf);
-                }
-                aes_encrypter_close(enc);
+            AESEncrypter *enc = aes_encrypter_new(key, data, (dav_read_func)cxBufferRead, NULL);
+            char buf[1024];
+            size_t r = 0;
+            while((r = aes_read(buf, 1, 1024, enc)) != 0) {
+                cxBufferWrite(buf, 1, r, cbuf);
+            }
+            aes_encrypter_close(enc);
 
-                AESDecrypter *dec = aes_decrypter_new(key, pbuf, (dav_write_func)cxBufferWrite);
-                aes_write(cbuf->space, 1, cbuf->pos, dec);
-                aes_decrypter_shutdown(dec);
-                aes_decrypter_close(dec);
-
-                CX_TEST_ASSERT(slen == pbuf->pos);
-                CX_TEST_ASSERT(!memcmp(strings[j], pbuf->space, slen));
+            AESDecrypter *dec = aes_decrypter_new(key, pbuf, (dav_write_func)cxBufferWrite);
+            aes_write(cbuf->space, 1, cbuf->pos, dec);
+            aes_decrypter_shutdown(dec);
+            aes_decrypter_close(dec);
 
-                data->pos = 0;
-                CxBuffer *enc2 = aes_encrypt_buffer(data, key);
-                CxBuffer *dec2 = aes_decrypt_buffer(enc2, key);
-
-                CX_TEST_ASSERT(dec2->size == data->size);
-                CX_TEST_ASSERT(!memcmp(strings[j], dec2->space, dec2->size));
-            }
+            UCX_TEST_ASSERT(slen == pbuf->pos, "wrong length after enc-dec");
+            UCX_TEST_ASSERT(!memcmp(strings[j], pbuf->space, slen), "wrong content after enc-dec");
+            
+            data->pos = 0;
+            CxBuffer *enc2 = aes_encrypt_buffer(data, key);
+            CxBuffer *dec2 = aes_decrypt_buffer(enc2, key);
+            
+            UCX_TEST_ASSERT(dec2->size == data->size, "dec2 has wrong size");
+            UCX_TEST_ASSERT(!memcmp(strings[j], dec2->space, dec2->size), "dec2 has wrong content");
         }
     }
+    
+    UCX_TEST_END;
 }
 
 static char *pws[] = {
@@ -409,50 +417,51 @@
 };
 
 
-CX_TEST(test_dav_pw2key) {
+UCX_TEST(test_dav_pw2key) {
     
-    CX_TEST_DO {
+    
+    UCX_TEST_BEGIN;
     
-        for(int p=0;p<4;p++) {
-            for(int s=0;s<4;s++) {
-                DavKey *keys[4];
-                keys[0] =  dav_pw2key(
-                        pws[p],
-                        salt[s],
-                        saltlen[s],
-                        DAV_PWFUNC_PBKDF2_SHA256,
-                        DAV_KEY_AES128);
-                keys[1] = dav_pw2key(
-                        pws[p],
-                        salt[s],
-                        saltlen[s],
-                        DAV_PWFUNC_PBKDF2_SHA256,
-                        DAV_KEY_AES256);
-                keys[2] = dav_pw2key(
-                        pws[p],
-                        salt[s],
-                        saltlen[s],
-                        DAV_PWFUNC_PBKDF2_SHA512,
-                        DAV_KEY_AES128);
-                keys[3] = dav_pw2key(
-                        pws[p],
-                        salt[s],
-                        saltlen[s],
-                        DAV_PWFUNC_PBKDF2_SHA512,
-                        DAV_KEY_AES256);
-
-                for(int i=0;i<4;i++) {
-                    DavKey *key = keys[i];
-
-                    int index = 16*p + 4*s + i;
-                    int keylen = index % 2 == 0 ? 16 : 32;
-
-                    CX_TEST_ASSERT(key);
-                    CX_TEST_ASSERT(keylen == key->length);
-                    CX_TEST_ASSERT(!memcmp(key->data, pwgenkeys[index], keylen));
-                }
+    for(int p=0;p<4;p++) {
+        for(int s=0;s<4;s++) {
+            DavKey *keys[4];
+            keys[0] =  dav_pw2key(
+                    pws[p],
+                    salt[s],
+                    saltlen[s],
+                    DAV_PWFUNC_PBKDF2_SHA256,
+                    DAV_KEY_AES128);
+            keys[1] = dav_pw2key(
+                    pws[p],
+                    salt[s],
+                    saltlen[s],
+                    DAV_PWFUNC_PBKDF2_SHA256,
+                    DAV_KEY_AES256);
+            keys[2] = dav_pw2key(
+                    pws[p],
+                    salt[s],
+                    saltlen[s],
+                    DAV_PWFUNC_PBKDF2_SHA512,
+                    DAV_KEY_AES128);
+            keys[3] = dav_pw2key(
+                    pws[p],
+                    salt[s],
+                    saltlen[s],
+                    DAV_PWFUNC_PBKDF2_SHA512,
+                    DAV_KEY_AES256);
+            
+            for(int i=0;i<4;i++) {
+                DavKey *key = keys[i];
+                
+                int index = 16*p + 4*s + i;
+                int keylen = index % 2 == 0 ? 16 : 32;
+                
+                UCX_TEST_ASSERT(key, "no key");
+                UCX_TEST_ASSERT(keylen == key->length, "wrong key length");
+                UCX_TEST_ASSERT(!memcmp(key->data, pwgenkeys[index], keylen), "wrong key data");
             }
         }
+    }
     
-    }
+    UCX_TEST_END;
 }
--- a/test/crypto.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/test/crypto.h	Sat Nov 08 23:06:11 2025 +0100
@@ -29,17 +29,17 @@
 #ifndef CRYPTO_H
 #define CRYPTO_H
 
-#include <cx/test.h>
+#include "test.h"
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
-CX_TEST(test_util_decrypt_str_k);
-CX_TEST(test_util_encrypt_str_k);
-CX_TEST(test_crypto_buffer);
-CX_TEST(test_crypto_stream);
-CX_TEST(test_dav_pw2key);
+UCX_TEST(test_util_decrypt_str_k);
+UCX_TEST(test_util_encrypt_str_k);
+UCX_TEST(test_crypto_buffer);
+UCX_TEST(test_crypto_stream);
+UCX_TEST(test_dav_pw2key);
 
 
 #ifdef __cplusplus
--- a/test/helper.c	Tue Oct 14 21:02:26 2025 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,34 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2025 Olaf Wintermann. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#include "helper.h"
-
-
-DavSession* test_session(DavContext *ctx) {
-    return dav_session_new(ctx, "http://localhost:8182/webdav/"); 
-}
--- a/test/helper.h	Tue Oct 14 21:02:26 2025 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,46 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2025 Olaf Wintermann. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#ifndef TEST_HELPER_H
-#define TEST_HELPER_H
-
-#include <libidav/webdav.h>
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-DavSession* test_session(DavContext *ctx);
-
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /* TEST_HELPER_H */
-
--- a/test/main.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/test/main.c	Sat Nov 08 23:06:11 2025 +0100
@@ -29,14 +29,12 @@
 #include <stdio.h>
 #include <stdlib.h>
 
-#include <cx/test.h>
+#include "test.h"
 
 #include "base64.h"
 #include "crypto.h"
 #include "utils.h"
 
-#include "test_resource.h"
-
 int test_main(int argc, char **argv);
 
 #ifdef _WIN32
@@ -53,24 +51,21 @@
     printf("libidav tests\n");
     printf("-------------\n\n");
     
-    CxTestSuite* suite = cx_test_suite_new("libidav");
+    UcxTestSuite* suite = ucx_test_suite_new();
     
-    cx_test_register(suite, test_util_base64decode);
-    cx_test_register(suite, test_util_base64decode_len);
-    cx_test_register(suite, test_util_base64encode);
-    cx_test_register(suite, test_util_decrypt_str_k);
-    cx_test_register(suite, test_util_encrypt_str_k);
-    cx_test_register(suite, test_crypto_buffer);
-    cx_test_register(suite, test_crypto_stream);
-    cx_test_register(suite, test_dav_pw2key);
-    cx_test_register(suite, test_util_parse_creationdate);
+    ucx_test_register(suite, test_util_base64decode);
+    ucx_test_register(suite, test_util_base64decode_len);
+    ucx_test_register(suite, test_util_base64encode);
+    ucx_test_register(suite, test_util_decrypt_str_k);
+    ucx_test_register(suite, test_util_encrypt_str_k);
+    ucx_test_register(suite, test_crypto_buffer);
+    ucx_test_register(suite, test_crypto_stream);
+    ucx_test_register(suite, test_dav_pw2key);
+    ucx_test_register(suite, test_util_parse_creationdate);
     
-    cx_test_register(suite, test_resource_iterator_visitor);
-    cx_test_register(suite, test_resource_map);
-    
-    cx_test_run(suite, stdout, (cx_write_func)fwrite);
+    ucx_test_run(suite, stdout);
     fflush(stdout);
-    cx_test_suite_free(suite);
+    ucx_test_suite_free(suite);
     
     return 0;
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/test.c	Sat Nov 08 23:06:11 2025 +0100
@@ -0,0 +1,91 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2017 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "test.h"
+
+UcxTestSuite* ucx_test_suite_new() {
+    UcxTestSuite* suite = (UcxTestSuite*) malloc(sizeof(UcxTestSuite));
+    if (suite != NULL) {
+        suite->success = 0;
+        suite->failure = 0;
+        suite->tests = NULL;
+    }
+
+    return suite;
+}
+
+void ucx_test_suite_free(UcxTestSuite* suite) {
+    UcxTestList *l = suite->tests;
+    while (l != NULL) {
+        UcxTestList *e = l;
+        l = l->next;
+        free(e);
+    }
+    free(suite);
+}
+
+int ucx_test_register(UcxTestSuite* suite, UcxTest test) {
+    if (suite->tests) {
+        UcxTestList *newelem = (UcxTestList*) malloc(sizeof(UcxTestList));
+        if (newelem) {
+            newelem->test = test;
+            newelem->next = NULL;
+            
+            UcxTestList *last = suite->tests;
+            while (last->next) {
+                last = last->next;
+            }
+            last->next = newelem;
+            
+            return EXIT_SUCCESS;
+        } else {
+            return EXIT_FAILURE;
+        }
+    } else {
+        suite->tests = (UcxTestList*) malloc(sizeof(UcxTestList));
+        if (suite->tests) {
+            suite->tests->test = test;
+            suite->tests->next = NULL;
+            
+            return EXIT_SUCCESS;
+        } else {
+            return EXIT_FAILURE;
+        }
+    }
+}
+
+void ucx_test_run(UcxTestSuite* suite, FILE* output) {
+    suite->success = 0;
+    suite->failure = 0;
+    for (UcxTestList* elem = suite->tests ; elem ; elem = elem->next) {
+        elem->test(suite, output);
+    }
+    fwrite("\nAll test completed.\n", 1, 21, output);
+    fprintf(output, "  Total:   %u\n  Success: %u\n  Failure: %u\n",
+            suite->success+suite->failure, suite->success, suite->failure);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/test.h	Sat Nov 08 23:06:11 2025 +0100
@@ -0,0 +1,241 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2017 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+ 
+/**
+ * @file: test.h
+ * 
+ * UCX Test Framework.
+ * 
+ * Usage of this test framework:
+ *
+ * **** IN HEADER FILE: ****
+ *
+ * <pre>
+ * UCX_TEST(function_name);
+ * UCX_TEST_SUBROUTINE(subroutine_name, paramlist); // optional
+ * </pre>
+ *
+ * **** IN SOURCE FILE: ****
+ * <pre>
+ * UCX_TEST_SUBROUTINE(subroutine_name, paramlist) {
+ *   // tests with UCX_TEST_ASSERT()
+ * }
+ * 
+ * UCX_TEST(function_name) {
+ *   // memory allocation and other stuff here
+ *   #UCX_TEST_BEGIN
+ *   // tests with UCX_TEST_ASSERT() and/or
+ *   // calls with UCX_TEST_CALL_SUBROUTINE() here
+ *   #UCX_TEST_END
+ *   // cleanup of memory here
+ * }
+ * </pre>
+ *
+ * <b>Note:</b> if a test fails, a longjump is performed
+ * back to the #UCX_TEST_BEGIN macro!
+ * 
+ * <b>Attention:</b> Do not call own functions within a test, that use
+ * UCX_TEST_ASSERT() macros and are not defined by using UCX_TEST_SUBROUTINE().
+ * 
+ *
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ *
+ */
+
+#ifndef UCX_21_TEST_H
+#define	UCX_21_TEST_H
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <setjmp.h>
+
+#ifdef	__cplusplus
+extern "C" {
+#endif
+
+#ifndef __FUNCTION__
+
+/**
+ * Alias for the <code>__func__</code> preprocessor macro.
+ * Some compilers use <code>__func__</code> and others use __FUNCTION__.
+ * We use __FUNCTION__ so we define it for those compilers which use
+ * <code>__func__</code>.
+ */
+#define __FUNCTION__ __func__
+#endif
+
+/** Type for the UcxTestSuite. */
+typedef struct UcxTestSuite UcxTestSuite;
+
+/** Pointer to a test function. */
+typedef void(*UcxTest)(UcxTestSuite*,FILE*);
+
+/** Type for the internal list of test cases. */
+typedef struct UcxTestList UcxTestList;
+
+/** Structure for the internal list of test cases. */
+struct UcxTestList {
+    
+    /** Test case. */
+    UcxTest test;
+    
+    /** Pointer to the next list element. */
+    UcxTestList *next;
+};
+
+/**
+ * A test suite containing multiple test cases.
+ */
+struct UcxTestSuite {
+    
+    /** The number of successful tests after the suite has been run. */
+    unsigned int success;
+    
+    /** The number of failed tests after the suite has been run. */
+    unsigned int failure;
+    
+    /**
+     * Internal list of test cases.
+     * Use ucx_test_register() to add tests to this list.
+     */
+    UcxTestList *tests;
+};
+
+/**
+ * Creates a new test suite.
+ * @return a new test suite
+ */
+UcxTestSuite* ucx_test_suite_new();
+
+/**
+ * Destroys a test suite.
+ * @param suite the test suite to destroy
+ */
+void ucx_test_suite_free(UcxTestSuite* suite);
+
+/**
+ * Registers a test function with the specified test suite.
+ * 
+ * @param suite the suite, the test function shall be added to
+ * @param test the test function to register
+ * @return <code>EXIT_SUCCESS</code> on success or
+ * <code>EXIT_FAILURE</code> on failure
+ */
+int ucx_test_register(UcxTestSuite* suite, UcxTest test);
+
+/**
+ * Runs a test suite and writes the test log to the specified stream.
+ * @param suite the test suite to run
+ * @param outstream the stream the log shall be written to
+ */
+void ucx_test_run(UcxTestSuite* suite, FILE* outstream);
+
+/**
+ * Macro for a #UcxTest function header.
+ * 
+ * Use this macro to declare and/or define a #UcxTest function.
+ * 
+ * @param name the name of the test function
+ */
+#define UCX_TEST(name) void name(UcxTestSuite* _suite_,FILE *_output_)
+
+/**
+ * Marks the begin of a test.
+ * <b>Note:</b> Any UCX_TEST_ASSERT() calls must be performed <b>after</b>
+ * #UCX_TEST_BEGIN.
+ * 
+ * @see #UCX_TEST_END
+ */
+#define UCX_TEST_BEGIN fwrite("Running ", 1, 8, _output_);\
+        fwrite(__FUNCTION__, 1, strlen(__FUNCTION__), _output_);\
+        fwrite("... ", 1, 4, _output_);\
+        jmp_buf _env_; \
+        if (!setjmp(_env_)) {
+
+/**
+ * Checks a test assertion.
+ * If the assertion is correct, the test carries on. If the assertion is not
+ * correct, the specified message (terminated by a dot and a line break) is
+ * written to the test suites output stream.
+ * @param condition the condition to check
+ * @param message the message that shall be printed out on failure
+ */
+#define UCX_TEST_ASSERT(condition,message) if (!(condition)) { \
+        fwrite(message".\n", 1, 2+strlen(message), _output_); \
+        _suite_->failure++; \
+        longjmp(_env_, 1);\
+    }
+
+/**
+ * Macro for a test subroutine function header.
+ * 
+ * Use this to declare and/or define a subroutine that can be called by using
+ * UCX_TEST_CALL_SUBROUTINE().
+ * 
+ * @param name the name of the subroutine
+ * @param ... the parameter list
+ * 
+ * @see UCX_TEST_CALL_SUBROUTINE()
+ */
+#define UCX_TEST_SUBROUTINE(name,...) void name(UcxTestSuite* _suite_,\
+        FILE *_output_, jmp_buf _env_, __VA_ARGS__)
+
+/**
+ * Macro for calling a test subroutine.
+ * 
+ * Subroutines declared with UCX_TEST_SUBROUTINE() can be called by using this
+ * macro.
+ * 
+ * <b>Note:</b> You may <b>only</b> call subroutines within a #UCX_TEST_BEGIN-
+ * #UCX_TEST_END-block.
+ * 
+ * @param name the name of the subroutine
+ * @param ... the argument list
+ * 
+ * @see UCX_TEST_SUBROUTINE()
+ */
+#define UCX_TEST_CALL_SUBROUTINE(name,...) \
+        name(_suite_,_output_,_env_,__VA_ARGS__);
+
+/**
+ * Marks the end of a test.
+ * <b>Note:</b> Any UCX_TEST_ASSERT() calls must be performed <b>before</b>
+ * #UCX_TEST_END.
+ * 
+ * @see #UCX_TEST_BEGIN
+ */
+#define UCX_TEST_END fwrite("success.\n", 1, 9, _output_); _suite_->success++;}
+
+#ifdef	__cplusplus
+}
+#endif
+
+#endif	/* UCX_21_TEST_H */
+
--- a/test/test_finfo.c	Tue Oct 14 21:02:26 2025 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2025 Olaf Wintermann. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#include "test_finfo.h"
-
--- a/test/test_finfo.h	Tue Oct 14 21:02:26 2025 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,46 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2025 Olaf Wintermann. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#ifndef TEST_FINFO_H
-#define TEST_FINFO_H
-
-#include <cx/test.h>
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-
-
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /* TEST_FINFO_H */
-
--- a/test/test_resource.c	Tue Oct 14 21:02:26 2025 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,166 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2025 Olaf Wintermann. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#include "test_resource.h"
-
-#include "helper.h"
-
-
-CX_TEST(test_resource_iterator_visitor) {
-    DavContext *ctx = dav_context_new();
-    DavSession *sn = test_session(ctx);
-    CX_TEST_DO {
-        DavResource *root = dav_resource_new(sn, "/");
-        DavResource *c1 = dav_resource_new(sn, "/child1");
-        DavResource *c2 = dav_resource_new(sn, "/child2");
-        DavResource *c1c1 = dav_resource_new(sn, "/child1/sub1");
-        DavResource *c1c2 = dav_resource_new(sn, "/child1/sub2");
-        DavResource *c1c2c1 = dav_resource_new(sn, "/child2/sub2/x1");
-        DavResource *c1c3 = dav_resource_new(sn, "/child1/sub3");
-        DavResource *c3 = dav_resource_new(sn, "/child3");
-        
-        root->children = c1;
-        c1->next = c2; c2->prev = c1;
-        c2->next = c3; c3->prev = c2;
-        
-        c1->parent = root;
-        c2->parent = root;
-        c3->parent = root;
-        
-        c1->children = c1c1;
-        c1c1->next = c1c2; c1c2->prev = c1c1;
-        c1c2->next = c1c3; c1c3->prev = c1c2;
-        
-        c1c1->parent = c1;
-        c1c2->parent = c1;
-        c1c3->parent = c1;
-        
-        c1c2->children = c1c2c1; c1c2c1->parent = c1c2;
-        
-        DavResource *resources[] = {root, c1, c2, c3, c1c1, c1c2, c1c3, c1c2c1 };
-        
-        // Iterator Test
-        CxTreeIterator i = dav_resource_iterator(root);
-        int iterated = 0;
-        cx_foreach(DavResource *, res, i) {
-            for(int r=0;r<8;r++) {
-                if(res == resources[r]) {
-                    res->exists = 1;
-                    break;
-                }
-            }
-            iterated++;
-        }
-        CX_TEST_ASSERT(iterated == 8);
-        
-        for(int ir=0;ir<8;ir++) {
-            CX_TEST_ASSERT(resources[ir]->exists);
-            resources[ir]->exists = 0; // reset for next test
-        }
-        
-        // Visitor Test
-        CxTreeVisitor v = dav_resource_visitor(root);
-        int visited = 0;
-        cx_foreach(DavResource *, res, v) {
-            for(int r=0;r<8;r++) {
-                if(res == resources[r]) {
-                    res->exists = 1;
-                    break;
-                }
-            }
-            visited++;
-        }
-        CX_TEST_ASSERT(visited == 8);
-        
-        for(int vr=0;vr<8;vr++) {
-            CX_TEST_ASSERT(resources[vr]->exists);
-            resources[vr]->exists = 0; // reset for next test
-        }
-        
-        
-        // partial test
-        i = dav_resource_iterator(c1);
-        iterated = 0;
-        cx_foreach(DavResource *, res, i) {
-            iterated++;
-        }
-        CX_TEST_ASSERT(iterated == 7);
-        
-        v = dav_resource_visitor(c1);
-        visited = 0;
-        cx_foreach(DavResource *, res, v) {
-            visited++;
-        }
-        CX_TEST_ASSERT(visited == 7);
-    }
-    dav_context_destroy(ctx);
-}
-
-CX_TEST(test_resource_map) {
-    DavContext *ctx = dav_context_new();
-    DavSession *sn = test_session(ctx);
-    CX_TEST_DO {
-        DavResource *root = dav_resource_new(sn, "/");
-        DavResource *c1 = dav_resource_new(sn, "/child1");
-        DavResource *c2 = dav_resource_new(sn, "/child2");
-        DavResource *c1c1 = dav_resource_new(sn, "/child1/sub1");
-        DavResource *c1c2 = dav_resource_new(sn, "/child1/sub2");
-        DavResource *c2c2c1 = dav_resource_new(sn, "/child2/sub2/x1");
-        DavResource *c1c3 = dav_resource_new(sn, "/child1/sub3");
-        DavResource *c3 = dav_resource_new(sn, "/child3");
-        
-        root->children = c1;
-        c1->next = c2; c2->prev = c1;
-        c2->next = c3; c3->prev = c2;
-        
-        c1->parent = root;
-        c2->parent = root;
-        c3->parent = root;
-        
-        c1->children = c1c1;
-        c1c1->next = c1c2; c1c2->prev = c1c1;
-        c1c2->next = c1c3; c1c3->prev = c1c2;
-        
-        c1c1->parent = c1;
-        c1c2->parent = c1;
-        c1c3->parent = c1;
-        
-        c1c2->children = c2c2c1; c2c2c1->parent = c1c2;
-        
-        CxMap *map = dav_resource_map(root);
-        CX_TEST_ASSERT(cxMapGet(map, "/") == root);
-        CX_TEST_ASSERT(cxMapGet(map, "/child1") == c1);
-        CX_TEST_ASSERT(cxMapGet(map, "/child2") == c2);
-        CX_TEST_ASSERT(cxMapGet(map, "/child3") == c3);
-        CX_TEST_ASSERT(cxMapGet(map, "/child1/sub1") == c1c1);
-        CX_TEST_ASSERT(cxMapGet(map, "/child1/sub2") == c1c2);
-        CX_TEST_ASSERT(cxMapGet(map, "/child1/sub3") == c1c3);
-        CX_TEST_ASSERT(cxMapGet(map, "/child2/sub2/x1") == c2c2c1);
-    }
-    dav_context_destroy(ctx);
-}
\ No newline at end of file
--- a/test/test_resource.h	Tue Oct 14 21:02:26 2025 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,47 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2025 Olaf Wintermann. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#ifndef TEST_RESOURCE_H
-#define TEST_RESOURCE_H
-
-#include <cx/test.h>
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-CX_TEST(test_resource_iterator_visitor);
-CX_TEST(test_resource_map);
-
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /* TEST_RESOURCE_H */
-
--- a/test/utils.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/test/utils.c	Sat Nov 08 23:06:11 2025 +0100
@@ -26,28 +26,33 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 
-#include "utils.h"
+#include "test.h"
+
 #include <libidav/utils.h>
 
-CX_TEST(test_util_parse_creationdate) {
-    CX_TEST_DO {
-        time_t t1 = util_parse_creationdate("2012-11-29T21:35:36Z");
-        time_t t2 = util_parse_creationdate("2023-09-17T13:03:00+02:00");
-
-        struct tm *tm = gmtime(&t1);
-        CX_TEST_ASSERT(tm->tm_year == 112);
-        CX_TEST_ASSERT(tm->tm_mon == 10);
-        CX_TEST_ASSERT(tm->tm_mday == 29);
-        CX_TEST_ASSERT(tm->tm_hour == 21);
-        CX_TEST_ASSERT(tm->tm_min == 35);
-        CX_TEST_ASSERT(tm->tm_sec == 36);
-
-        tm = gmtime(&t2);
-        CX_TEST_ASSERT(tm->tm_year == 123);
-        CX_TEST_ASSERT(tm->tm_mon == 8);
-        CX_TEST_ASSERT(tm->tm_mday == 17);
-        CX_TEST_ASSERT(tm->tm_hour == 11);
-        CX_TEST_ASSERT(tm->tm_min == 03);
-        CX_TEST_ASSERT(tm->tm_sec == 0);
-    }  
+UCX_TEST(test_util_parse_creationdate) {
+    UCX_TEST_BEGIN;
+    
+    time_t t1 = util_parse_creationdate("2012-11-29T21:35:36Z");
+    time_t t2 = util_parse_creationdate("2023-09-17T13:03:00+02:00");
+    
+    struct tm *tm = gmtime(&t1);
+    UCX_TEST_ASSERT(tm->tm_year == 112, "t1: wrong year");
+    UCX_TEST_ASSERT(tm->tm_mon == 10, "t1: wrong month");
+    UCX_TEST_ASSERT(tm->tm_mday == 29, "t1: wrong day");
+    UCX_TEST_ASSERT(tm->tm_hour == 21, "t1: wrong hour");
+    UCX_TEST_ASSERT(tm->tm_min == 35, "t1: wrong minute");
+    UCX_TEST_ASSERT(tm->tm_sec == 36, "t1: wrong second");
+    
+    tm = gmtime(&t2);
+    UCX_TEST_ASSERT(tm->tm_year == 123, "t2: wrong year");
+    UCX_TEST_ASSERT(tm->tm_mon == 8, "t2: wrong month");
+    UCX_TEST_ASSERT(tm->tm_mday == 17, "t2: wrong day");
+    UCX_TEST_ASSERT(tm->tm_hour == 11, "t2: wrong hour");
+    UCX_TEST_ASSERT(tm->tm_min == 03, "t2: wrong minute");
+    UCX_TEST_ASSERT(tm->tm_sec == 0, "t2: wrong second");
+    
+    UCX_TEST_END;
+    
+    
 }
--- a/test/utils.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/test/utils.h	Sat Nov 08 23:06:11 2025 +0100
@@ -29,13 +29,13 @@
 #ifndef TEST_UTILS_H
 #define TEST_UTILS_H
 
-#include <cx/test.h>
+#include "test.h"
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
-CX_TEST(test_util_parse_creationdate);
+UCX_TEST(test_util_parse_creationdate);
 
 
 #ifdef __cplusplus
--- a/ucx/Makefile	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/Makefile	Sat Nov 08 23:06:11 2025 +0100
@@ -46,6 +46,7 @@
 SRC += streams.c
 SRC += properties.c
 SRC += json.c
+SRC += kv_list.c
 
 OBJ   = $(SRC:%.c=../build/ucx/%$(OBJ_EXT))
 
--- a/ucx/allocator.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/allocator.c	Sat Nov 08 23:06:11 2025 +0100
@@ -73,7 +73,7 @@
         NULL
 };
 const CxAllocator * const cxStdlibAllocator = &cx_stdlib_allocator;
-const CxAllocator * cxDefaultAllocator = cxStdlibAllocator;
+const CxAllocator * cxDefaultAllocator = &cx_stdlib_allocator;
 
 int cx_reallocate_(
         void **mem,
--- a/ucx/array_list.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/array_list.c	Sat Nov 08 23:06:11 2025 +0100
@@ -50,7 +50,7 @@
 }
 
 CxArrayReallocator cx_array_default_reallocator_impl = {
-        cx_array_default_realloc, NULL, NULL, 0, 0
+        cx_array_default_realloc, NULL, NULL
 };
 
 CxArrayReallocator *cx_array_default_reallocator = &cx_array_default_reallocator_impl;
@@ -72,11 +72,11 @@
     }
 
     // retrieve the pointer to the actual allocator
-    const CxAllocator *al = alloc->ptr1;
+    const CxAllocator *al = alloc->allocator;
 
     // check if the array is still located on the stack
     void *newmem;
-    if (array == alloc->ptr2) {
+    if (array == alloc->stack_ptr) {
         newmem = cxMalloc(al, n);
         if (newmem != NULL && array != NULL) {
             memcpy(newmem, array, old_capacity*elem_size);
@@ -89,20 +89,27 @@
 
 struct cx_array_reallocator_s cx_array_reallocator(
         const struct cx_allocator_s *allocator,
-        const void *stackmem
+        const void *stack_ptr
 ) {
     if (allocator == NULL) {
         allocator = cxDefaultAllocator;
     }
     return (struct cx_array_reallocator_s) {
             cx_array_advanced_realloc,
-            (void*) allocator, (void*) stackmem,
-            0, 0
+            allocator, stack_ptr,
     };
 }
 
 // LOW LEVEL ARRAY LIST FUNCTIONS
 
+/**
+ * Increases the capacity until it is a multiple of a some alignment or reaches the maximum.
+ *
+ * @param cap the required capacity (must not be larger than @p max)
+ * @param alignment the alignment
+ * @param max the maximum capacity
+ * @return the aligned capacity
+ */
 static size_t cx_array_align_capacity(
         size_t cap,
         size_t alignment,
@@ -291,7 +298,7 @@
                 *target, oldcap, newcap, elem_size, reallocator
         );
         if (newmem == NULL) {
-            return 1;
+            return 1; // LCOV_EXCL_LINE
         }
 
         // repair src pointer, if necessary
@@ -335,7 +342,7 @@
     return 0;
 }
 
-int cx_array_insert_sorted(
+static int cx_array_insert_sorted_impl(
         void **target,
         size_t *size,
         size_t *capacity,
@@ -343,7 +350,8 @@
         const void *sorted_data,
         size_t elem_size,
         size_t elem_count,
-        CxArrayReallocator *reallocator
+        CxArrayReallocator *reallocator,
+        bool allow_duplicates
 ) {
     // assert pointers
     assert(target != NULL);
@@ -369,6 +377,7 @@
     // store some counts
     size_t old_size = *size;
     size_t old_capacity = *capacity;
+    // the necessary capacity is the worst case assumption, including duplicates
     size_t needed_capacity = old_size + elem_count;
 
     // if we need more than we have, try a reallocation
@@ -418,13 +427,60 @@
                 bptr,
                 cmp_func
         );
+        // binary search gives us the smallest index;
+        // we also want to include equal elements here
+        while (si + copy_len < elem_count
+                && cmp_func(bptr, src+copy_len*elem_size) == 0) {
+            copy_len++;
+        }
 
         // copy the source elements
-        bytes_copied = copy_len * elem_size;
-        memcpy(dest, src, bytes_copied);
-        dest += bytes_copied;
-        src += bytes_copied;
-        si += copy_len;
+        if (copy_len > 0) {
+            if (allow_duplicates) {
+                // we can copy the entire chunk
+                bytes_copied = copy_len * elem_size;
+                memcpy(dest, src, bytes_copied);
+                dest += bytes_copied;
+                src += bytes_copied;
+                si += copy_len;
+                di += copy_len;
+            } else {
+                // first, check the end of the source chunk
+                // for being a duplicate of the bptr
+                const char *end_of_src = src + (copy_len - 1) * elem_size;
+                size_t skip_len = 0;
+                while (copy_len > 0 && cmp_func(bptr, end_of_src) == 0) {
+                    end_of_src -= elem_size;
+                    skip_len++;
+                    copy_len--;
+                }
+                char *last = dest == *target ? NULL : dest - elem_size;
+                // then iterate through the source chunk
+                // and skip all duplicates with the last element in the array
+                size_t more_skipped = 0;
+                for (unsigned j = 0; j < copy_len; j++) {
+                    if (last != NULL && cmp_func(last, src) == 0) {
+                        // duplicate - skip
+                        src += elem_size;
+                        si++;
+                        more_skipped++;
+                    } else {
+                        memcpy(dest, src, elem_size);
+                        src += elem_size;
+                        last = dest;
+                        dest += elem_size;
+                        si++;
+                        di++;
+                    }
+                }
+                // skip the previously identified elements as well
+                src += skip_len * elem_size;
+                si += skip_len;
+                skip_len += more_skipped;
+                // reduce the actual size by the number of skipped elements
+                *size -= skip_len;
+            }
+        }
 
         // when all source elements are in place, we are done
         if (si >= elem_count) break;
@@ -443,20 +499,103 @@
         memmove(dest, bptr, bytes_copied);
         dest += bytes_copied;
         bptr += bytes_copied;
+        di += copy_len;
         bi += copy_len;
     }
 
-    // still source elements left? simply append them
+    // still source elements left?
     if (si < elem_count) {
-        memcpy(dest, src, elem_size * (elem_count - si));
+        if (allow_duplicates) {
+            // duplicates allowed or nothing inserted yet: simply copy everything
+            memcpy(dest, src, elem_size * (elem_count - si));
+        } else {
+            if (dest != *target) {
+                // skip all source elements that equal the last element
+                char *last = dest - elem_size;
+                while (si < elem_count) {
+                    if (last != NULL && cmp_func(last, src) == 0) {
+                        src += elem_size;
+                        si++;
+                        (*size)--;
+                    } else {
+                        break;
+                    }
+                }
+            }
+            // we must check the elements in the chunk one by one
+            while (si < elem_count) {
+                // find a chain of elements that can be copied
+                size_t copy_len = 1, skip_len = 0;
+                {
+                    const char *left_src = src;
+                    while (si + copy_len < elem_count) {
+                        const char *right_src = left_src + elem_size;
+                        int d = cmp_func(left_src,  right_src);
+                        if (d < 0) {
+                            if (skip_len > 0) {
+                                // new larger element found;
+                                // handle it in the next cycle
+                                break;
+                            }
+                            left_src += elem_size;
+                            copy_len++;
+                        } else if (d == 0) {
+                            left_src += elem_size;
+                            skip_len++;
+                        } else {
+                            break;
+                        }
+                    }
+                }
+                size_t bytes_copied = copy_len * elem_size;
+                memcpy(dest, src, bytes_copied);
+                dest += bytes_copied;
+                src += bytes_copied + skip_len * elem_size;
+                si += copy_len + skip_len;
+                di += copy_len;
+                *size -= skip_len;
+            }
+        }
     }
 
-    // still buffer elements left?
-    // don't worry, we already moved them to the correct place
+    // buffered elements need to be moved when we skipped duplicates
+    size_t total_skipped = new_size - *size;
+    if (bi < new_size && total_skipped > 0) {
+        // move the remaining buffer to the end of the array
+        memmove(dest, bptr, elem_size * (new_size - bi));
+    }
 
     return 0;
 }
 
+int cx_array_insert_sorted(
+        void **target,
+        size_t *size,
+        size_t *capacity,
+        cx_compare_func cmp_func,
+        const void *sorted_data,
+        size_t elem_size,
+        size_t elem_count,
+        CxArrayReallocator *reallocator
+) {
+    return cx_array_insert_sorted_impl(target, size, capacity,
+        cmp_func, sorted_data, elem_size, elem_count, reallocator, true);
+}
+
+int cx_array_insert_unique(
+        void **target,
+        size_t *size,
+        size_t *capacity,
+        cx_compare_func cmp_func,
+        const void *sorted_data,
+        size_t elem_size,
+        size_t elem_count,
+        CxArrayReallocator *reallocator
+) {
+    return cx_array_insert_sorted_impl(target, size, capacity,
+        cmp_func, sorted_data, elem_size, elem_count, reallocator, false);
+}
+
 size_t cx_array_binary_search_inf(
         const void *arr,
         size_t size,
@@ -502,6 +641,13 @@
         result = cmp_func(elem, arr_elem);
         if (result == 0) {
             // found it!
+            // check previous elements;
+            // when they are equal, report the smallest index
+            arr_elem -= elem_size;
+            while (pivot_index > 0 && cmp_func(elem, arr_elem) == 0) {
+                pivot_index--;
+                arr_elem -= elem_size;
+            }
             return pivot_index;
         } else if (result < 0) {
             // element is smaller than pivot, continue search left
@@ -644,7 +790,7 @@
     // guarantee enough capacity
     if (arl->capacity < list->collection.size + n) {
         size_t new_capacity = list->collection.size + n;
-        new_capacity = new_capacity - (new_capacity % 16) + 16;
+        new_capacity = cx_array_align_capacity(new_capacity, 16, SIZE_MAX);
         if (cxReallocateArray(
                 list->collection.allocator,
                 &arl->data, new_capacity,
@@ -700,6 +846,31 @@
     }
 }
 
+static size_t cx_arl_insert_unique(
+        struct cx_list_s *list,
+        const void *sorted_data,
+        size_t n
+) {
+    // get a correctly typed pointer to the list
+    cx_array_list *arl = (cx_array_list *) list;
+
+    if (cx_array_insert_unique(
+            &arl->data,
+            &list->collection.size,
+            &arl->capacity,
+            list->collection.cmpfunc,
+            sorted_data,
+            list->collection.elem_size,
+            n,
+            &arl->reallocator
+    )) {
+        // array list implementation is "all or nothing"
+        return 0;
+    } else {
+        return n;
+    }
+}
+
 static void *cx_arl_insert_element(
         struct cx_list_s *list,
         size_t index,
@@ -717,7 +888,7 @@
         const void *elem,
         int prepend
 ) {
-    struct cx_list_s *list = iter->src_handle.m;
+    struct cx_list_s *list = iter->src_handle;
     if (iter->index < list->collection.size) {
         if (cx_arl_insert_element(list,
                 iter->index + 1 - prepend, elem) == NULL) {
@@ -928,7 +1099,7 @@
 
 static bool cx_arl_iter_valid(const void *it) {
     const struct cx_iterator_s *iter = it;
-    const struct cx_list_s *list = iter->src_handle.c;
+    const struct cx_list_s *list = iter->src_handle;
     return iter->index < list->collection.size;
 }
 
@@ -941,23 +1112,25 @@
     struct cx_iterator_s *iter = it;
     if (iter->base.remove) {
         iter->base.remove = false;
-        cx_arl_remove(iter->src_handle.m, iter->index, 1, NULL);
+        cx_arl_remove(iter->src_handle, iter->index, 1, NULL);
+        iter->elem_count--;
     } else {
         iter->index++;
         iter->elem_handle =
                 ((char *) iter->elem_handle)
-                + ((const struct cx_list_s *) iter->src_handle.c)->collection.elem_size;
+                + ((const struct cx_list_s *) iter->src_handle)->collection.elem_size;
     }
 }
 
 static void cx_arl_iter_prev(void *it) {
     struct cx_iterator_s *iter = it;
-    const cx_array_list *list = iter->src_handle.c;
     if (iter->base.remove) {
         iter->base.remove = false;
-        cx_arl_remove(iter->src_handle.m, iter->index, 1, NULL);
+        cx_arl_remove(iter->src_handle, iter->index, 1, NULL);
+        iter->elem_count--;
     }
     iter->index--;
+    cx_array_list *list = iter->src_handle;
     if (iter->index < list->base.collection.size) {
         iter->elem_handle = ((char *) list->data)
                             + iter->index * list->base.collection.elem_size;
@@ -973,7 +1146,7 @@
     struct cx_iterator_s iter;
 
     iter.index = index;
-    iter.src_handle.c = list;
+    iter.src_handle = (void*)list;
     iter.elem_handle = cx_arl_at(list, index);
     iter.elem_size = list->collection.elem_size;
     iter.elem_count = list->collection.size;
@@ -981,7 +1154,7 @@
     iter.base.current = cx_arl_iter_current;
     iter.base.next = backwards ? cx_arl_iter_prev : cx_arl_iter_next;
     iter.base.remove = false;
-    iter.base.mutating = false;
+    iter.base.allow_remove = true;
 
     return iter;
 }
@@ -991,6 +1164,7 @@
         cx_arl_insert_element,
         cx_arl_insert_array,
         cx_arl_insert_sorted,
+        cx_arl_insert_unique,
         cx_arl_insert_iter,
         cx_arl_remove,
         cx_arl_clear,
--- a/ucx/compare.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/compare.c	Sat Nov 08 23:06:11 2025 +0100
@@ -198,6 +198,20 @@
     return cx_vcmp_uint64(a, b);
 }
 
+int cx_vcmp_size(size_t a, size_t b) {
+    if (a == b) {
+        return 0;
+    } else {
+        return a < b ? -1 : 1;
+    }
+}
+
+int cx_cmp_size(const void *i1, const void *i2) {
+    size_t a = *((const size_t *) i1);
+    size_t b = *((const size_t *) i2);
+    return cx_vcmp_size(a, b);
+}
+
 int cx_vcmp_float(float a, float b) {
     if (fabsf(a - b) < 1e-6f) {
         return 0;
--- a/ucx/cx/allocator.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/cx/allocator.h	Sat Nov 08 23:06:11 2025 +0100
@@ -46,36 +46,22 @@
     /**
      * The allocator's malloc() implementation.
      */
-    void *(*malloc)(
-            void *data,
-            size_t n
-    );
+    void *(*malloc)(void *data, size_t n);
 
     /**
      * The allocator's realloc() implementation.
      */
-    void *(*realloc)(
-            void *data,
-            void *mem,
-            size_t n
-    );
+    void *(*realloc)(void *data, void *mem, size_t n);
 
     /**
      * The allocator's calloc() implementation.
      */
-    void *(*calloc)(
-            void *data,
-            size_t nmemb,
-            size_t size
-    );
+    void *(*calloc)(void *data, size_t nmemb, size_t size);
 
     /**
      * The allocator's free() implementation.
      */
-    void (*free)(
-            void *data,
-            void *mem
-    );
+    void (*free)(void *data, void *mem);
 } cx_allocator_class;
 
 /**
@@ -100,15 +86,13 @@
 /**
  * A pre-defined allocator using standard library malloc() etc.
  */
-cx_attr_export
-extern const CxAllocator * const cxStdlibAllocator;
+CX_EXPORT extern const CxAllocator * const cxStdlibAllocator;
 
 /**
  * The default allocator that is used by UCX.
  * Initialized with cxStdlibAllocator, but you may change it.
  */
-cx_attr_export
-extern const CxAllocator * cxDefaultAllocator;
+CX_EXPORT extern const CxAllocator * cxDefaultAllocator;
 
 /**
  * Function pointer type for destructor functions.
@@ -133,10 +117,33 @@
  * @param data an optional pointer to custom data
  * @param memory a pointer to the object to destruct
   */
-typedef void (*cx_destructor_func2)(
-        void *data,
-        void *memory
-);
+typedef void (*cx_destructor_func2)(void *data, void *memory);
+
+
+/**
+ * Function pointer type for clone functions.
+ *
+ * A clone function is supposed to create a deep copy of the memory pointed to
+ * by the @p source pointer.
+ * If the @p target pointer is non-null, the clone function is supposed to store
+ * the copy into that memory region.
+ * Otherwise, the clone function shall use the specified @p allocator to create
+ * a new object.
+ *
+ * The return value of a clone function is always a pointer to the target
+ * memory region, or @c NULL if any allocation failed.
+ * A clone function SHOULD NOT fail for any other reason than an allocation
+ * failure.
+ *
+ * @param target the target memory or @c NULL, if memory shall be allocated
+ * @param source the source memory
+ * @param allocator the allocator that shall be used
+ * @param data optional additional data
+ * @return either the specified @p target, a pointer to the allocated memory,
+ * or @c NULL, if any error occurred
+ */
+typedef void*(cx_clone_func)(void *target, const void *source,
+                             const CxAllocator *allocator, void *data);
 
 /**
  * Reallocate a previously allocated block and changes the pointer in-place,
@@ -153,13 +160,8 @@
  * @retval non-zero failure
  * @see cx_reallocatearray()
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-int cx_reallocate_(
-        void **mem,
-        size_t n
-);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT int cx_reallocate_(void **mem, size_t n);
 
 /**
  * Reallocate a previously allocated block and changes the pointer in-place,
@@ -180,14 +182,8 @@
  * @retval non-zero failure
  * @see cx_reallocate()
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-int cx_reallocatearray_(
-        void **mem,
-        size_t nmemb,
-        size_t size
-);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT int cx_reallocatearray_(void **mem, size_t nmemb, size_t size);
 
 /**
  * Reallocate a previously allocated block and changes the pointer in-place,
@@ -244,11 +240,7 @@
  * @param mem a pointer to the block to free
  */
 cx_attr_nonnull_arg(1)
-cx_attr_export
-void cxFree(
-        const CxAllocator *allocator,
-        void *mem
-);
+CX_EXPORT void cxFree(const CxAllocator *allocator, void *mem);
 
 /**
  * Allocate @p n bytes of memory.
@@ -257,21 +249,14 @@
  * @param n the number of bytes
  * @return a pointer to the allocated memory
  */
-cx_attr_nodiscard
-cx_attr_nonnull
-cx_attr_malloc
-cx_attr_dealloc_ucx
-cx_attr_allocsize(2)
-cx_attr_export
-void *cxMalloc(
-        const CxAllocator *allocator,
-        size_t n
-);
+cx_attr_nodiscard cx_attr_nonnull
+cx_attr_malloc cx_attr_dealloc_ucx cx_attr_allocsize(2)
+CX_EXPORT void *cxMalloc(const CxAllocator *allocator, size_t n);
 
 /**
  * Reallocate the previously allocated block in @p mem, making the new block
  * @p n bytes long.
- * This function may return the same pointer that was passed to it, if moving
+ * This function may return the same pointer passed to it if moving
  * the memory was not necessary.
  *
  * @note Re-allocating a block allocated by a different allocator is undefined.
@@ -281,25 +266,18 @@
  * @param n the new size in bytes
  * @return a pointer to the reallocated memory
  */
-cx_attr_nodiscard
-cx_attr_nonnull_arg(1)
-cx_attr_dealloc_ucx
-cx_attr_allocsize(3)
-cx_attr_export
-void *cxRealloc(
-        const CxAllocator *allocator,
-        void *mem,
-        size_t n
-);
+cx_attr_nodiscard cx_attr_nonnull_arg(1)
+cx_attr_dealloc_ucx cx_attr_allocsize(3)
+CX_EXPORT void *cxRealloc(const CxAllocator *allocator, void *mem, size_t n);
 
 /**
  * Reallocate the previously allocated block in @p mem, making the new block
  * @p n bytes long.
- * This function may return the same pointer that was passed to it, if moving
+ * This function may return the same pointer passed to it if moving
  * the memory was not necessary.
  *
  * The size is calculated by multiplying @p nemb and @p size.
- * If that multiplication overflows, this function returns @c NULL and @c errno
+ * If that multiplication overflows, this function returns @c NULL, and @c errno
  * will be set.
  *
  * @note Re-allocating a block allocated by a different allocator is undefined.
@@ -310,17 +288,10 @@
  * @param size the size of each element
  * @return a pointer to the reallocated memory
  */
-cx_attr_nodiscard
-cx_attr_nonnull_arg(1)
-cx_attr_dealloc_ucx
-cx_attr_allocsize(3, 4)
-cx_attr_export
-void *cxReallocArray(
-        const CxAllocator *allocator,
-        void *mem,
-        size_t nmemb,
-        size_t size
-);
+cx_attr_nodiscard cx_attr_nonnull_arg(1)
+cx_attr_dealloc_ucx cx_attr_allocsize(3, 4)
+CX_EXPORT void *cxReallocArray(const CxAllocator *allocator,
+        void *mem, size_t nmemb, size_t size);
 
 /**
  * Reallocate a previously allocated block and changes the pointer in-place,
@@ -330,7 +301,7 @@
  * @note Re-allocating a block allocated by a different allocator is undefined.
  *
  * @par Error handling
- * @c errno will be set, if the underlying realloc function does so.
+ * @c errno will be set if the underlying realloc function does so.
  *
  * @param allocator the allocator
  * @param mem pointer to the pointer to allocated block
@@ -338,14 +309,8 @@
  * @retval zero success
  * @retval non-zero failure
  */
-cx_attr_nodiscard
-cx_attr_nonnull
-cx_attr_export
-int cxReallocate_(
-        const CxAllocator *allocator,
-        void **mem,
-        size_t n
-);
+cx_attr_nodiscard cx_attr_nonnull
+CX_EXPORT int cxReallocate_(const CxAllocator *allocator, void **mem, size_t n);
 
 /**
  * Reallocate a previously allocated block and changes the pointer in-place,
@@ -355,7 +320,7 @@
  * @note Re-allocating a block allocated by a different allocator is undefined.
  *
  * @par Error handling
- * @c errno will be set, if the underlying realloc function does so.
+ * @c errno will be set if the underlying realloc function does so.
  *
  * @param allocator (@c CxAllocator*) the allocator
  * @param mem (@c void**) pointer to the pointer to allocated block
@@ -385,15 +350,9 @@
  * @retval zero success
  * @retval non-zero on failure
  */
-cx_attr_nodiscard
-cx_attr_nonnull
-cx_attr_export
-int cxReallocateArray_(
-        const CxAllocator *allocator,
-        void **mem,
-        size_t nmemb,
-        size_t size
-);
+cx_attr_nodiscard cx_attr_nonnull
+CX_EXPORT int cxReallocateArray_(const CxAllocator *allocator,
+        void **mem, size_t nmemb, size_t size);
 
 /**
  * Reallocate a previously allocated block and changes the pointer in-place,
@@ -425,17 +384,9 @@
  * @param size the size of each element in bytes
  * @return a pointer to the allocated memory
  */
-cx_attr_nonnull_arg(1)
-cx_attr_nodiscard
-cx_attr_malloc
-cx_attr_dealloc_ucx
-cx_attr_allocsize(2, 3)
-cx_attr_export
-void *cxCalloc(
-        const CxAllocator *allocator,
-        size_t nmemb,
-        size_t size
-);
+cx_attr_nonnull_arg(1) cx_attr_nodiscard
+cx_attr_malloc cx_attr_dealloc_ucx cx_attr_allocsize(2, 3)
+CX_EXPORT void *cxCalloc(const CxAllocator *allocator, size_t nmemb, size_t size);
 
 /**
  * Allocate @p n bytes of memory and sets every byte to zero.
@@ -444,16 +395,9 @@
  * @param n the number of bytes
  * @return a pointer to the allocated memory
  */
-cx_attr_nodiscard
-cx_attr_nonnull
-cx_attr_malloc
-cx_attr_dealloc_ucx
-cx_attr_allocsize(2)
-cx_attr_export
-void *cxZalloc(
-        const CxAllocator *allocator,
-        size_t n
-);
+cx_attr_nodiscard cx_attr_nonnull
+cx_attr_malloc cx_attr_dealloc_ucx cx_attr_allocsize(2)
+CX_EXPORT void *cxZalloc(const CxAllocator *allocator, size_t n);
 
 /**
  * Convenience macro that invokes cxMalloc() with the cxDefaultAllocator.
--- a/ucx/cx/array_list.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/cx/array_list.h	Sat Nov 08 23:06:11 2025 +0100
@@ -44,11 +44,10 @@
 #endif
 
 /**
- * The maximum item size in an array list that fits into stack buffer
- * when swapped.
+ * The maximum item size in an array list that fits into
+ * a stack buffer when swapped.
  */
-cx_attr_export
-extern const unsigned cx_array_swap_sbo_size;
+CX_EXPORT extern const unsigned cx_array_swap_sbo_size;
 
 /**
  * Declares variables for an array that can be used with the convenience macros.
@@ -84,7 +83,7 @@
 /**
  * Declares variables for an array that can be used with the convenience macros.
  *
- * The size and capacity variables will have @c size_t type.
+ * The size and capacity variables will have type @c size_t.
  * Use #CX_ARRAY_DECLARE_SIZED() to specify a different type.
  *
  * @par Examples
@@ -147,7 +146,7 @@
  * const CxAllocator *al = // ...
  * cx_array_initialize_a(al, myarray, 128);
  * // ...
- * cxFree(al, myarray); // don't forget to free with same allocator
+ * cxFree(al, myarray); // remember to free with the same allocator
  * @endcode
  *
  * @param allocator (@c CxAllocator*) the allocator
@@ -172,10 +171,9 @@
      * Reallocates space for the given array.
      *
      * Implementations are not required to free the original array.
-     * This allows reallocation of static memory by allocating heap memory
-     * and copying the array contents. The information in the custom fields of
-     * the referenced allocator can be used to track the state of the memory
-     * or to transport other additional data.
+     * This allows reallocation of static or stack memory by allocating heap memory
+     * and copying the array contents; namely when @c stack_ptr in this struct
+     * is not @c NULL and @p array equals @c stack_ptr.
      *
      * @param array the array to reallocate
      * @param old_capacity the old number of elements
@@ -184,33 +182,18 @@
      * @param alloc a reference to this allocator
      * @return a pointer to the reallocated memory or @c NULL on failure
      */
-    cx_attr_nodiscard
-    cx_attr_nonnull_arg(5)
-    cx_attr_allocsize(3, 4)
-    void *(*realloc)(
-            void *array,
-            size_t old_capacity,
-            size_t new_capacity,
-            size_t elem_size,
-            struct cx_array_reallocator_s *alloc
-    );
+    void *(*realloc)( void *array, size_t old_capacity, size_t new_capacity,
+            size_t elem_size, struct cx_array_reallocator_s *alloc);
 
     /**
-     * Custom data pointer.
+     * The allocator that shall be used for the reallocations.
      */
-    void *ptr1;
-    /**
-     * Custom data pointer.
-     */
-    void *ptr2;
+    const CxAllocator *allocator;
     /**
-     * Custom data integer.
+     * Optional pointer to stack memory
+     * if the array is originally located on the stack.
      */
-    size_t int1;
-    /**
-     * Custom data integer.
-     */
-    size_t int2;
+    const void *stack_ptr;
 };
 
 /**
@@ -221,32 +204,28 @@
 /**
  * A default array reallocator that is based on the cxDefaultAllocator.
  */
-cx_attr_export
-extern CxArrayReallocator *cx_array_default_reallocator;
+CX_EXPORT extern CxArrayReallocator *cx_array_default_reallocator;
 
 /**
  * Creates a new array reallocator.
  *
  * When @p allocator is @c NULL, the cxDefaultAllocator will be used.
  *
- * When @p stackmem is not @c NULL, the reallocator is supposed to be used
- * @em only for the specific array that is initially located at @p stackmem.
- * When reallocation is needed, the reallocator checks, if the array is
- * still located at @p stackmem and copies the contents to the heap.
+ * When @p stack_ptr is not @c NULL, the reallocator is supposed to be used
+ * @em only for the specific array initially located at @p stack_ptr.
+ * When reallocation is needed, the reallocator checks if the array is
+ * still located at @p stack_ptr and copies the contents to the heap.
  *
- * @note Invoking this function with both arguments @c NULL will return a
+ * @note Invoking this function with both arguments being @c NULL will return a
  * reallocator that behaves like #cx_array_default_reallocator.
  *
  * @param allocator the allocator this reallocator shall be based on
- * @param stackmem the address of the array when the array is initially located
- * on the stack or shall not reallocated in place
+ * @param stack_ptr the address of the array when the array is initially located
+ * on the stack or shall not reallocate in place
  * @return an array reallocator
  */
-cx_attr_export
-CxArrayReallocator cx_array_reallocator(
-        const struct cx_allocator_s *allocator,
-        const void *stackmem
-);
+CX_EXPORT CxArrayReallocator cx_array_reallocator(
+        const struct cx_allocator_s *allocator, const void *stack_ptr);
 
 /**
  * Reserves memory for additional elements.
@@ -263,7 +242,7 @@
  *
  * The @p width in bytes refers to the size and capacity.
  * Both must have the same width.
- * Supported are 0, 1, 2, and 4, as well as 8 if running on a 64 bit
+ * Supported are 0, 1, 2, and 4, as well as 8 if running on a 64-bit
  * architecture. If set to zero, the native word width is used.
  *
  * @param array a pointer to the target array
@@ -279,16 +258,9 @@
  * @see cx_array_reallocator()
  */
 cx_attr_nonnull_arg(1, 2, 3)
-cx_attr_export
-int cx_array_reserve(
-        void **array,
-        void *size,
-        void *capacity,
-        unsigned width,
-        size_t elem_size,
-        size_t elem_count,
-        CxArrayReallocator *reallocator
-);
+CX_EXPORT int cx_array_reserve(void **array, void *size, void *capacity,
+        unsigned width, size_t elem_size, size_t elem_count,
+        CxArrayReallocator *reallocator);
 
 /**
  * Copies elements from one array to another.
@@ -296,7 +268,7 @@
  * The elements are copied to the @p target array at the specified @p index,
  * overwriting possible elements. The @p index does not need to be in range of
  * the current array @p size. If the new index plus the number of elements added
- * would extend the array's size, the remaining @p capacity is used.
+ * extends the array's size, the remaining @p capacity is used.
  *
  * If the @p capacity is also insufficient to hold the new data, a reallocation
  * attempt is made with the specified @p reallocator.
@@ -305,7 +277,7 @@
  *
  * The @p width in bytes refers to the size and capacity.
  * Both must have the same width.
- * Supported are 0, 1, 2, and 4, as well as 8 if running on a 64 bit
+ * Supported are 0, 1, 2, and 4, as well as 8 if running on a 64-bit
  * architecture. If set to zero, the native word width is used.
  *
  * @param target a pointer to the target array
@@ -323,18 +295,9 @@
  * @see cx_array_reallocator()
  */
 cx_attr_nonnull_arg(1, 2, 3, 6)
-cx_attr_export
-int cx_array_copy(
-        void **target,
-        void *size,
-        void *capacity,
-        unsigned width,
-        size_t index,
-        const void *src,
-        size_t elem_size,
-        size_t elem_count,
-        CxArrayReallocator *reallocator
-);
+CX_EXPORT int cx_array_copy(void **target, void *size, void *capacity, unsigned width,
+        size_t index, const void *src, size_t elem_size, size_t elem_count,
+        CxArrayReallocator *reallocator);
 
 /**
  * Convenience macro that uses cx_array_copy() with a default layout and
@@ -482,17 +445,9 @@
  * @retval non-zero failure
  */
 cx_attr_nonnull_arg(1, 2, 3, 5)
-cx_attr_export
-int cx_array_insert_sorted(
-        void **target,
-        size_t *size,
-        size_t *capacity,
-        cx_compare_func cmp_func,
-        const void *src,
-        size_t elem_size,
-        size_t elem_count,
-        CxArrayReallocator *reallocator
-);
+CX_EXPORT int cx_array_insert_sorted(void **target, size_t *size, size_t *capacity,
+        cx_compare_func cmp_func, const void *src, size_t elem_size, size_t elem_count,
+        CxArrayReallocator *reallocator);
 
 /**
  * Inserts an element into a sorted array.
@@ -500,7 +455,7 @@
  * If the target array is not already sorted with respect
  * to the specified @p cmp_func, the behavior is undefined.
  *
- * If the capacity is insufficient to hold the new data, a reallocation
+ * If the capacity is not enough to hold the new data, a reallocation
  * attempt is made.
  *
  * The \@ SIZE_TYPE is flexible and can be any unsigned integer type.
@@ -586,6 +541,127 @@
 #define cx_array_simple_insert_sorted(array, src, n, cmp_func) \
     cx_array_simple_insert_sorted_a(NULL, array, src, n, cmp_func)
 
+
+/**
+ * Inserts a sorted array into another sorted array, avoiding duplicates.
+ *
+ * If either the target or the source array is not already sorted with respect
+ * to the specified @p cmp_func, the behavior is undefined.
+ *
+ * If the capacity is insufficient to hold the new data, a reallocation
+ * attempt is made.
+ * You can create your own reallocator by hand, use #cx_array_default_reallocator,
+ * or use the convenience function cx_array_reallocator() to create a custom reallocator.
+ *
+ * @param target a pointer to the target array
+ * @param size a pointer to the size of the target array
+ * @param capacity a pointer to the capacity of the target array
+ * @param cmp_func the compare function for the elements
+ * @param src the source array
+ * @param elem_size the size of one element
+ * @param elem_count the number of elements to insert
+ * @param reallocator the array reallocator to use
+ * (@c NULL defaults to #cx_array_default_reallocator)
+ * @retval zero success
+ * @retval non-zero failure
+ */
+cx_attr_nonnull_arg(1, 2, 3, 5)
+CX_EXPORT int cx_array_insert_unique(void **target, size_t *size, size_t *capacity,
+        cx_compare_func cmp_func, const void *src, size_t elem_size, size_t elem_count,
+        CxArrayReallocator *reallocator);
+
+/**
+ * Inserts an element into a sorted array if it does not exist.
+ *
+ * If the target array is not already sorted with respect
+ * to the specified @p cmp_func, the behavior is undefined.
+ *
+ * If the capacity is insufficient to hold the new data, a reallocation
+ * attempt is made.
+ *
+ * The \@ SIZE_TYPE is flexible and can be any unsigned integer type.
+ * It is important, however, that @p size and @p capacity are pointers to
+ * variables of the same type.
+ *
+ * @param target (@c void**) a pointer to the target array
+ * @param size (@c SIZE_TYPE*) a pointer to the size of the target array
+ * @param capacity (@c SIZE_TYPE*) a pointer to the capacity of the target array
+ * @param elem_size (@c size_t) the size of one element
+ * @param elem (@c void*) a pointer to the element to add
+ * @param cmp_func (@c cx_cmp_func) the compare function for the elements
+ * @param reallocator (@c CxArrayReallocator*) the array reallocator to use
+ * @retval zero success (also when the element was already present)
+ * @retval non-zero failure
+ */
+#define cx_array_add_unique(target, size, capacity, elem_size, elem, cmp_func, reallocator) \
+    cx_array_insert_unique((void**)(target), size, capacity, cmp_func, elem, elem_size, 1, reallocator)
+
+/**
+ * Convenience macro for cx_array_add_unique() with a default
+ * layout and the specified reallocator.
+ *
+ * @param reallocator (@c CxArrayReallocator*) the array reallocator to use
+ * @param array the name of the array (NOT a pointer or alias to the array)
+ * @param elem the element to add (NOT a pointer, address is automatically taken)
+ * @param cmp_func (@c cx_cmp_func) the compare function for the elements
+ * @retval zero success
+ * @retval non-zero failure
+ * @see CX_ARRAY_DECLARE()
+ * @see cx_array_simple_add_unique()
+ */
+#define cx_array_simple_add_unique_a(reallocator, array, elem, cmp_func) \
+    cx_array_add_unique(&array, &(array##_size), &(array##_capacity), \
+        sizeof((array)[0]), &(elem), cmp_func, reallocator)
+
+/**
+ * Convenience macro for cx_array_add_unique() with a default
+ * layout and the default reallocator.
+ *
+ * @param array the name of the array (NOT a pointer or alias to the array)
+ * @param elem the element to add (NOT a pointer, address is automatically taken)
+ * @param cmp_func (@c cx_cmp_func) the compare function for the elements
+ * @retval zero success
+ * @retval non-zero failure
+ * @see CX_ARRAY_DECLARE()
+ * @see cx_array_simple_add_unique_a()
+ */
+#define cx_array_simple_add_unique(array, elem, cmp_func) \
+    cx_array_simple_add_unique_a(NULL, array, elem, cmp_func)
+
+/**
+ * Convenience macro for cx_array_insert_unique() with a default
+ * layout and the specified reallocator.
+ *
+ * @param reallocator (@c CxArrayReallocator*) the array reallocator to use
+ * @param array the name of the array (NOT a pointer or alias to the array)
+ * @param src (@c void*) pointer to the source array
+ * @param n (@c size_t) number of elements in the source array
+ * @param cmp_func (@c cx_cmp_func) the compare function for the elements
+ * @retval zero success
+ * @retval non-zero failure
+ * @see CX_ARRAY_DECLARE()
+ * @see cx_array_simple_insert_unique()
+ */
+#define cx_array_simple_insert_unique_a(reallocator, array, src, n, cmp_func) \
+    cx_array_insert_unique((void**)(&array), &(array##_size), &(array##_capacity), \
+        cmp_func, src, sizeof((array)[0]), n, reallocator)
+
+/**
+ * Convenience macro for cx_array_insert_unique() with a default
+ * layout and the default reallocator.
+ *
+ * @param array the name of the array (NOT a pointer or alias to the array)
+ * @param src (@c void*) pointer to the source array
+ * @param n (@c size_t) number of elements in the source array
+ * @param cmp_func (@c cx_cmp_func) the compare function for the elements
+ * @retval zero success
+ * @retval non-zero failure
+ * @see CX_ARRAY_DECLARE()
+ * @see cx_array_simple_insert_unique_a()
+ */
+#define cx_array_simple_insert_unique(array, src, n, cmp_func) \
+    cx_array_simple_insert_unique_a(NULL, array, src, n, cmp_func)
+
 /**
  * Searches the largest lower bound in a sorted array.
  *
@@ -609,14 +685,8 @@
  * @see cx_array_binary_search()
  */
 cx_attr_nonnull
-cx_attr_export
-size_t cx_array_binary_search_inf(
-        const void *arr,
-        size_t size,
-        size_t elem_size,
-        const void *elem,
-        cx_compare_func cmp_func
-);
+CX_EXPORT size_t cx_array_binary_search_inf(const void *arr, size_t size,
+        size_t elem_size, const void *elem, cx_compare_func cmp_func);
 
 /**
  * Searches an item in a sorted array.
@@ -635,14 +705,8 @@
  * @see cx_array_binary_search_sup()
  */
 cx_attr_nonnull
-cx_attr_export
-size_t cx_array_binary_search(
-        const void *arr,
-        size_t size,
-        size_t elem_size,
-        const void *elem,
-        cx_compare_func cmp_func
-);
+CX_EXPORT size_t cx_array_binary_search(const void *arr, size_t size,
+        size_t elem_size, const void *elem, cx_compare_func cmp_func);
 
 /**
  * Searches the smallest upper bound in a sorted array.
@@ -667,37 +731,25 @@
  * @see cx_array_binary_search()
  */
 cx_attr_nonnull
-cx_attr_export
-size_t cx_array_binary_search_sup(
-        const void *arr,
-        size_t size,
-        size_t elem_size,
-        const void *elem,
-        cx_compare_func cmp_func
-);
+CX_EXPORT size_t cx_array_binary_search_sup(const void *arr, size_t size,
+        size_t elem_size, const void *elem, cx_compare_func cmp_func);
 
 /**
  * Swaps two array elements.
  *
  * @param arr the array
  * @param elem_size the element size
- * @param idx1 index of first element
- * @param idx2 index of second element
+ * @param idx1 index of the first element
+ * @param idx2 index of the second element
  */
 cx_attr_nonnull
-cx_attr_export
-void cx_array_swap(
-        void *arr,
-        size_t elem_size,
-        size_t idx1,
-        size_t idx2
-);
+CX_EXPORT void cx_array_swap(void *arr, size_t elem_size, size_t idx1, size_t idx2);
 
 /**
  * Allocates an array list for storing elements with @p elem_size bytes each.
  *
  * If @p elem_size is #CX_STORE_POINTERS, the created list stores pointers instead of
- * copies of the added elements and the compare function will be automatically set
+ * copies of the added elements, and the compare function will be automatically set
  * to cx_cmp_ptr(), if none is given.
  *
  * @param allocator the allocator for allocating the list memory
@@ -712,13 +764,8 @@
 cx_attr_nodiscard
 cx_attr_malloc
 cx_attr_dealloc(cxListFree, 1)
-cx_attr_export
-CxList *cxArrayListCreate(
-        const CxAllocator *allocator,
-        cx_compare_func comparator,
-        size_t elem_size,
-        size_t initial_capacity
-);
+CX_EXPORT CxList *cxArrayListCreate(const CxAllocator *allocator,
+        cx_compare_func comparator, size_t elem_size, size_t initial_capacity);
 
 /**
  * Allocates an array list for storing elements with @p elem_size bytes each.
--- a/ucx/cx/buffer.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/cx/buffer.h	Sat Nov 08 23:06:11 2025 +0100
@@ -62,7 +62,7 @@
  * If this flag is enabled, the buffer will automatically free its contents when destroyed.
  *
  * Do NOT set this flag together with #CX_BUFFER_COPY_ON_WRITE. It will be automatically
- * set when the copy-on-write operations is performed.
+ * set when the copy-on-write operation is performed.
  */
 #define CX_BUFFER_FREE_CONTENTS 0x01
 
@@ -74,7 +74,7 @@
 /**
  * If this flag is enabled, the buffer will allocate new memory when written to.
  *
- * The current contents of the buffer will be copied to the new memory and the flag
+ * The current contents of the buffer will be copied to the new memory, and the flag
  * will be cleared while the #CX_BUFFER_FREE_CONTENTS flag will be set automatically.
  */
 #define CX_BUFFER_COPY_ON_WRITE 0x04
@@ -127,7 +127,7 @@
     size_t blkmax;
 
     /**
-     * The target for write function.
+     * The target for the write function.
      */
     void *target;
 
@@ -202,7 +202,7 @@
  * you will need to cast the pointer, and you should set the
  * #CX_BUFFER_COPY_ON_WRITE flag.
  *
- * You need to set the size manually after initialization, if
+ * You need to set the size manually after initialization if
  * you provide @p space which already contains data.
  *
  * When you specify stack memory as @p space and decide to use
@@ -210,7 +210,7 @@
  * #CX_BUFFER_COPY_ON_EXTEND flag, instead of the
  * #CX_BUFFER_AUTO_EXTEND flag.
  *
- * @note You may provide @c NULL as argument for @p space.
+ * @note You may provide @c NULL as the argument for @p space.
  * Then this function will allocate the space and enforce
  * the #CX_BUFFER_FREE_CONTENTS flag. In that case, specifying
  * copy-on-write should be avoided, because the allocated
@@ -227,14 +227,8 @@
  * @return zero on success, non-zero if a required allocation failed
  */
 cx_attr_nonnull_arg(1)
-cx_attr_export
-int cxBufferInit(
-        CxBuffer *buffer,
-        void *space,
-        size_t capacity,
-        const CxAllocator *allocator,
-        int flags
-);
+CX_EXPORT int cxBufferInit(CxBuffer *buffer, void *space, size_t capacity,
+        const CxAllocator *allocator, int flags);
 
 /**
  * Configures the buffer for flushing.
@@ -251,11 +245,7 @@
  * @see cxBufferWrite()
  */
 cx_attr_nonnull
-cx_attr_export
-int cxBufferEnableFlushing(
-    CxBuffer *buffer,
-    CxBufferFlushConfig config
-);
+CX_EXPORT int cxBufferEnableFlushing(CxBuffer *buffer, CxBufferFlushConfig config);
 
 /**
  * Destroys the buffer contents.
@@ -267,8 +257,7 @@
  * @see cxBufferInit()
  */
 cx_attr_nonnull
-cx_attr_export
-void cxBufferDestroy(CxBuffer *buffer);
+CX_EXPORT void cxBufferDestroy(CxBuffer *buffer);
 
 /**
  * Deallocates the buffer.
@@ -276,14 +265,10 @@
  * If the #CX_BUFFER_FREE_CONTENTS feature is enabled, this function also destroys
  * the contents. If you @em only want to destroy the contents, use cxBufferDestroy().
  *
- * @remark As with all free() functions, this accepts @c NULL arguments in which
- * case it does nothing.
- *
  * @param buffer the buffer to deallocate
  * @see cxBufferCreate()
  */
-cx_attr_export
-void cxBufferFree(CxBuffer *buffer);
+CX_EXPORT void cxBufferFree(CxBuffer *buffer);
 
 /**
  * Allocates and initializes a fresh buffer.
@@ -296,7 +281,7 @@
  * #CX_BUFFER_COPY_ON_EXTEND flag, instead of the
  * #CX_BUFFER_AUTO_EXTEND flag.
  *
- * @note You may provide @c NULL as argument for @p space.
+ * @note You may provide @c NULL as the argument for @p space.
  * Then this function will allocate the space and enforce
  * the #CX_BUFFER_FREE_CONTENTS flag.
  *
@@ -309,16 +294,9 @@
  * @param flags buffer features (see cx_buffer_s.flags)
  * @return a pointer to the buffer on success, @c NULL if a required allocation failed
  */
-cx_attr_malloc
-cx_attr_dealloc(cxBufferFree, 1)
-cx_attr_nodiscard
-cx_attr_export
-CxBuffer *cxBufferCreate(
-        void *space,
-        size_t capacity,
-        const CxAllocator *allocator,
-        int flags
-);
+cx_attr_malloc cx_attr_dealloc(cxBufferFree, 1) cx_attr_nodiscard
+CX_EXPORT CxBuffer *cxBufferCreate(void *space, size_t capacity,
+        const CxAllocator *allocator, int flags);
 
 /**
  * Shifts the contents of the buffer by the given offset.
@@ -327,8 +305,8 @@
  * If auto extension is enabled, the buffer grows, if necessary.
  * In case the auto extension fails, this function returns a non-zero value and
  * no contents are changed.
- * If auto extension is disabled, the contents that do not fit into the buffer
- * are discarded.
+ * When the auto extension is disabled, the contents that do not fit into the
+ * buffer are discarded.
  *
  * If the offset is negative, the contents are shifted to the left where the
  * first @p shift bytes are discarded.
@@ -336,15 +314,15 @@
  * If this value is larger than the buffer size, the buffer is emptied (but
  * not cleared, see the security note below).
  *
- * The buffer position gets shifted alongside with the content but is kept
+ * The buffer position gets shifted alongside the content but is kept
  * within the boundaries of the buffer.
  *
  * @note For situations where @c off_t is not large enough, there are specialized cxBufferShiftLeft() and
- * cxBufferShiftRight() functions using a @c size_t as parameter type.
+ * cxBufferShiftRight() functions using a @c size_t as the parameter type.
  *
  * @attention
  * Security Note: The shifting operation does @em not erase the previously occupied memory cells.
- * But you can easily do that manually, e.g. by calling
+ * But you can do that manually by calling
  * <code>memset(buffer->bytes, 0, shift)</code> for a right shift or
  * <code>memset(buffer->bytes + buffer->size, 0, buffer->capacity - buffer->size)</code>
  * for a left shift.
@@ -357,11 +335,7 @@
  * @see cxBufferShiftRight()
  */
 cx_attr_nonnull
-cx_attr_export
-int cxBufferShift(
-        CxBuffer *buffer,
-        off_t shift
-);
+CX_EXPORT int cxBufferShift(CxBuffer *buffer, off_t shift);
 
 /**
  * Shifts the buffer to the right.
@@ -374,11 +348,7 @@
  * @see cxBufferShift()
  */
 cx_attr_nonnull
-cx_attr_export
-int cxBufferShiftRight(
-        CxBuffer *buffer,
-        size_t shift
-);
+CX_EXPORT int cxBufferShiftRight(CxBuffer *buffer, size_t shift);
 
 /**
  * Shifts the buffer to the left.
@@ -391,11 +361,7 @@
  * @see cxBufferShift()
  */
 cx_attr_nonnull
-cx_attr_export
-int cxBufferShiftLeft(
-        CxBuffer *buffer,
-        size_t shift
-);
+CX_EXPORT int cxBufferShiftLeft(CxBuffer *buffer, size_t shift);
 
 
 /**
@@ -419,12 +385,7 @@
  *
  */
 cx_attr_nonnull
-cx_attr_export
-int cxBufferSeek(
-        CxBuffer *buffer,
-        off_t offset,
-        int whence
-);
+CX_EXPORT int cxBufferSeek(CxBuffer *buffer, off_t offset, int whence);
 
 /**
  * Clears the buffer by resetting the position and deleting the data.
@@ -439,8 +400,7 @@
  * @see cxBufferReset()
  */
 cx_attr_nonnull
-cx_attr_export
-void cxBufferClear(CxBuffer *buffer);
+CX_EXPORT void cxBufferClear(CxBuffer *buffer);
 
 /**
  * Resets the buffer by resetting the position and size to zero.
@@ -452,8 +412,7 @@
  * @see cxBufferClear()
  */
 cx_attr_nonnull
-cx_attr_export
-void cxBufferReset(CxBuffer *buffer);
+CX_EXPORT void cxBufferReset(CxBuffer *buffer);
 
 /**
  * Tests, if the buffer position has exceeded the buffer size.
@@ -463,10 +422,8 @@
  * byte of the buffer's contents
  * @retval false otherwise
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-bool cxBufferEof(const CxBuffer *buffer);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT bool cxBufferEof(const CxBuffer *buffer);
 
 
 /**
@@ -484,11 +441,7 @@
  * @see cxBufferShrink()
  */
 cx_attr_nonnull
-cx_attr_export
-int cxBufferMinimumCapacity(
-        CxBuffer *buffer,
-        size_t capacity
-);
+CX_EXPORT int cxBufferMinimumCapacity(CxBuffer *buffer, size_t capacity);
 
 /**
  * Shrinks the capacity of the buffer to fit its current size.
@@ -507,17 +460,13 @@
  * @see cxBufferMinimumCapacity()
  */
 cx_attr_nonnull
-cx_attr_export
-void cxBufferShrink(
-        CxBuffer *buffer,
-        size_t reserve
-);
+CX_EXPORT void cxBufferShrink(CxBuffer *buffer, size_t reserve);
 
 /**
  * Writes data to a CxBuffer.
  *
  * If automatic flushing is not enabled, the data is simply written into the
- * buffer at the current position and the position of the buffer is increased
+ * buffer at the current position, and the position of the buffer is increased
  * by the number of bytes written.
  *
  * If flushing is enabled and the buffer needs to flush, the data is flushed to
@@ -526,7 +475,7 @@
  * data in this buffer is shifted to the beginning of this buffer so that the
  * newly available space can be used to append as much data as possible.
  *
- * This function only stops writing more elements, when the flush target and this
+ * This function only stops writing more elements when the flush target and this
  * buffer are both incapable of taking more data or all data has been written.
  *
  * If, after flushing, the number of items that shall be written still exceeds
@@ -534,14 +483,14 @@
  * to the flush target, if possible.
  *
  * The number returned by this function is the number of elements from
- * @c ptr that could be written to either the flush target or the buffer
- * (so it does not include the number of items that had been already in the buffer
- * in were flushed during the process).
+ * @c ptr that could be written to either the flush target or the buffer.
+ * That means it does @em not include the number of items that were already in
+ * the buffer and were also flushed during the process.
  *
  * @attention
  * When @p size is larger than one and the contents of the buffer are not aligned
  * with @p size, flushing stops after all complete items have been flushed, leaving
- * the mis-aligned part in the buffer.
+ * the misaligned part in the buffer.
  * Afterward, this function only writes as many items as possible to the buffer.
  *
  * @note The signature is compatible with the fwrite() family of functions.
@@ -555,13 +504,8 @@
  * @see cxBufferRead()
  */
 cx_attr_nonnull
-cx_attr_export
-size_t cxBufferWrite(
-        const void *ptr,
-        size_t size,
-        size_t nitems,
-        CxBuffer *buffer
-);
+CX_EXPORT size_t cxBufferWrite(const void *ptr, size_t size,
+        size_t nitems, CxBuffer *buffer);
 
 /**
  * Appends data to a CxBuffer.
@@ -583,13 +527,8 @@
  * @see cxBufferRead()
  */
 cx_attr_nonnull
-cx_attr_export
-size_t cxBufferAppend(
-        const void *ptr,
-        size_t size,
-        size_t nitems,
-        CxBuffer *buffer
-);
+CX_EXPORT size_t cxBufferAppend(const void *ptr, size_t size,
+        size_t nitems, CxBuffer *buffer);
 
 /**
  * Performs a single flush-run on the specified buffer.
@@ -614,19 +553,19 @@
  * at position 200. The flush configuration is
  * @c blkmax=4 and @c blksize=64 .
  * Assume that the entire flush operation is successful.
- * All 200 bytes on the left hand-side from the current
+ * All 200 bytes on the left-hand-side from the current
  * position are written.
- * That means, the size of the buffer is now 140 and the
+ * That means the size of the buffer is now 140 and the
  * position is zero.
  *
  * @par Example 2
  * Same as Example 1, but now the @c blkmax is 1.
- * The size of the buffer is now 276 and the position is 136.
+ * The size of the buffer is now 276, and the position is 136.
  *
  * @par Example 3
  * Same as Example 1, but now assume the flush target
  * only accepts 100 bytes before returning zero.
- * That means, the flush operations manages to flush
+ * That means the flush operation manages to flush
  * one complete block and one partial block, ending
  * up with a buffer with size 240 and position 100.
  *
@@ -636,8 +575,8 @@
  * @remark When the buffer uses copy-on-write, the memory
  * is copied first, before attempting any flush.
  * This is, however, considered an erroneous use of the
- * buffer, because it does not make much sense to put
- * readonly data into an UCX buffer for flushing, instead
+ * buffer because it makes little sense to put
+ * readonly data into an UCX buffer for flushing instead
  * of writing it directly to the target.
  *
  * @param buffer the buffer
@@ -645,8 +584,7 @@
  * @see cxBufferEnableFlushing()
  */
 cx_attr_nonnull
-cx_attr_export
-size_t cxBufferFlush(CxBuffer *buffer);
+CX_EXPORT size_t cxBufferFlush(CxBuffer *buffer);
 
 /**
  * Reads data from a CxBuffer.
@@ -664,13 +602,8 @@
  * @see cxBufferAppend()
  */
 cx_attr_nonnull
-cx_attr_export
-size_t cxBufferRead(
-        void *ptr,
-        size_t size,
-        size_t nitems,
-        CxBuffer *buffer
-);
+CX_EXPORT size_t cxBufferRead(void *ptr, size_t size,
+        size_t nitems, CxBuffer *buffer);
 
 /**
  * Writes a character to a buffer.
@@ -678,9 +611,9 @@
  * The least significant byte of the argument is written to the buffer. If the
  * end of the buffer is reached and #CX_BUFFER_AUTO_EXTEND feature is enabled,
  * the buffer capacity is extended by cxBufferMinimumCapacity(). If the feature
- * is disabled or buffer extension fails, @c EOF is returned.
+ * is disabled or the buffer extension fails, @c EOF is returned.
  *
- * On successful write, the position of the buffer is increased.
+ * On successful writing, the position of the buffer is increased.
  *
  * If you just want to write a null-terminator at the current position, you
  * should use cxBufferTerminate() instead.
@@ -688,15 +621,11 @@
  * @param buffer the buffer to write to
  * @param c the character to write
  * @return the byte that has been written or @c EOF when the end of the stream is
- * reached and automatic extension is not enabled or not possible
+ * reached, and automatic extension is not enabled or not possible
  * @see cxBufferTerminate()
  */
 cx_attr_nonnull
-cx_attr_export
-int cxBufferPut(
-        CxBuffer *buffer,
-        int c
-);
+CX_EXPORT int cxBufferPut(CxBuffer *buffer, int c);
 
 /**
  * Writes a terminating zero to a buffer at the current position.
@@ -710,8 +639,7 @@
  * @return zero, if the terminator could be written, non-zero otherwise
  */
 cx_attr_nonnull
-cx_attr_export
-int cxBufferTerminate(CxBuffer *buffer);
+CX_EXPORT int cxBufferTerminate(CxBuffer *buffer);
 
 /**
  * Writes a string to a buffer.
@@ -722,13 +650,8 @@
  * @param str the zero-terminated string
  * @return the number of bytes written
  */
-cx_attr_nonnull
-cx_attr_cstr_arg(2)
-cx_attr_export
-size_t cxBufferPutString(
-        CxBuffer *buffer,
-        const char *str
-);
+cx_attr_nonnull cx_attr_cstr_arg(2)
+CX_EXPORT size_t cxBufferPutString(CxBuffer *buffer, const char *str);
 
 /**
  * Gets a character from a buffer.
@@ -739,8 +662,7 @@
  * @return the character or @c EOF, if the end of the buffer is reached
  */
 cx_attr_nonnull
-cx_attr_export
-int cxBufferGet(CxBuffer *buffer);
+CX_EXPORT int cxBufferGet(CxBuffer *buffer);
 
 #ifdef __cplusplus
 }
--- a/ucx/cx/collection.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/cx/collection.h	Sat Nov 08 23:06:11 2025 +0100
@@ -142,14 +142,24 @@
 /**
  * Indicates whether the collection can guarantee that the stored elements are currently sorted.
  *
- * This may return false even when the elements are sorted.
- * It is totally up to the implementation of the collection whether it keeps track of the order of its elements.
+ * This may return @c false even when the elements are sorted.
+ * It is totally up to the implementation of the collection when to check if the elements are sorted.
+ * It is usually a good practice to establish this property as an invariant that does not need
+ * to be re-checked on certain operations.
  *
  * @param c a pointer to a struct that contains #CX_COLLECTION_BASE
  * @retval true if the elements are currently sorted wrt. the collection's compare function
  * @retval false if the order of elements is unknown
  */
-#define cxCollectionSorted(c) ((c)->collection.sorted)
+#define cxCollectionSorted(c) ((c)->collection.sorted || (c)->collection.size == 0)
+
+/**
+ * Sets the compare function for a collection.
+ *
+ * @param c a pointer to a struct that contains #CX_COLLECTION_BASE
+ * @param func (@c cx_compare_func) the compare function that shall be used by @c c
+ */
+#define cxCollectionCompareFunc(c, func) (c)->collection.cmpfunc = (func)
 
 /**
  * Sets a simple destructor function for this collection.
--- a/ucx/cx/common.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/cx/common.h	Sat Nov 08 23:06:11 2025 +0100
@@ -49,7 +49,7 @@
  * <a href="https://uap-core.de/hg/ucx">https://uap-core.de/hg/ucx</a>
  * </p>
  *
- * <h2>LICENCE</h2>
+ * <h2>LICENSE</h2>
  *
  * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
  *
@@ -189,7 +189,7 @@
  */
 #define cx_attr_cstr_arg(idx)
 /**
- * No support for access attribute in clang.
+ * No support for the access attribute in clang.
  */
 #define cx_attr_access(mode, ...)
 #else
@@ -265,15 +265,32 @@
 #define _Thread_local __declspec(thread)
 #endif // _MSC_VER
 
+// ---------------------------------------------------------------------------
+//       Exported and inlined functions
+// ---------------------------------------------------------------------------
+
 #if defined(CX_WINDLL_EXPORT)
-#define cx_attr_export __declspec(dllexport)
+#define CX_EXPORT __declspec(dllexport)
 #elif defined(CX_WINDLL)
-#define cx_attr_export __declspec(dllimport)
+#define CX_EXPORT __declspec(dllimport)
 #else
 /** Only used for building Windows DLLs. */
-#define cx_attr_export
+#define CX_EXPORT
 #endif // CX_WINDLL / CX_WINDLL_EXPORT
 
+#ifdef __GNUC__
+/**
+ * Declares a function to be inlined.
+ */
+#define CX_INLINE __attribute__((always_inline)) static inline
+#else
+#define CX_INLINE static inline
+#endif
+/**
+ * Declares a compatibility function for C++ builds.
+ */
+#define CX_CPPDECL static inline
+
 // ---------------------------------------------------------------------------
 //       Useful function pointers
 // ---------------------------------------------------------------------------
@@ -281,22 +298,12 @@
 /**
  * Function pointer compatible with fwrite-like functions.
  */
-typedef size_t (*cx_write_func)(
-        const void *,
-        size_t,
-        size_t,
-        void *
-);
+typedef size_t (*cx_write_func)(const void*, size_t, size_t, void*);
 
 /**
  * Function pointer compatible with fread-like functions.
  */
-typedef size_t (*cx_read_func)(
-        void *,
-        size_t,
-        size_t,
-        void *
-);
+typedef size_t (*cx_read_func)(void*, size_t, size_t, void*);
 
 // ---------------------------------------------------------------------------
 //       Utility macros
@@ -348,9 +355,7 @@
 #if __cplusplus
 extern "C"
 #endif
-cx_attr_export int cx_szmul_impl(size_t a, size_t b, size_t *result);
+CX_EXPORT int cx_szmul_impl(size_t a, size_t b, size_t *result);
 #endif // cx_szmul
 
-
-
 #endif // UCX_COMMON_H
--- a/ucx/cx/compare.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/cx/compare.h	Sat Nov 08 23:06:11 2025 +0100
@@ -47,20 +47,14 @@
  *
  * All functions from compare.h with the cx_cmp prefix are
  * compatible with this signature and can be used as
- * compare function for collections, or other implementations
+ * compare function for collections or other implementations
  * that need to be type-agnostic.
  *
  * For simple comparisons the cx_vcmp family of functions
  * can be used, but they are NOT compatible with this function
  * pointer.
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-typedef int (*cx_compare_func)(
-    const void *left,
-    const void *right
-);
+typedef int (*cx_compare_func)(const void *left, const void *right);
 
 /**
  * Compares two integers of type int.
@@ -74,10 +68,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-int cx_cmp_int(const void *i1, const void *i2);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT int cx_cmp_int(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type int.
@@ -89,8 +81,7 @@
  * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
-cx_attr_export
-int cx_vcmp_int(int i1, int i2);
+CX_EXPORT int cx_vcmp_int(int i1, int i2);
 
 /**
  * Compares two integers of type long int.
@@ -104,10 +95,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-int cx_cmp_longint(const void *i1, const void *i2);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT int cx_cmp_longint(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type long int.
@@ -119,8 +108,7 @@
  * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
-cx_attr_export
-int cx_vcmp_longint(long int i1, long int i2);
+CX_EXPORT int cx_vcmp_longint(long int i1, long int i2);
 
 /**
  * Compares two integers of type long long.
@@ -134,10 +122,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-int cx_cmp_longlong(const void *i1, const void *i2);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT int cx_cmp_longlong(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type long long.
@@ -149,8 +135,7 @@
  * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
-cx_attr_export
-int cx_vcmp_longlong(long long int i1, long long int i2);
+CX_EXPORT int cx_vcmp_longlong(long long int i1, long long int i2);
 
 /**
  * Compares two integers of type int16_t.
@@ -164,10 +149,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-int cx_cmp_int16(const void *i1, const void *i2);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT int cx_cmp_int16(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type int16_t.
@@ -179,8 +162,7 @@
  * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
-cx_attr_export
-int cx_vcmp_int16(int16_t i1, int16_t i2);
+CX_EXPORT int cx_vcmp_int16(int16_t i1, int16_t i2);
 
 /**
  * Compares two integers of type int32_t.
@@ -194,10 +176,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-int cx_cmp_int32(const void *i1, const void *i2);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT int cx_cmp_int32(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type int32_t.
@@ -209,8 +189,7 @@
  * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
-cx_attr_export
-int cx_vcmp_int32(int32_t i1, int32_t i2);
+CX_EXPORT int cx_vcmp_int32(int32_t i1, int32_t i2);
 
 /**
  * Compares two integers of type int64_t.
@@ -224,10 +203,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-int cx_cmp_int64(const void *i1, const void *i2);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT int cx_cmp_int64(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type int64_t.
@@ -239,8 +216,7 @@
  * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
-cx_attr_export
-int cx_vcmp_int64(int64_t i1, int64_t i2);
+CX_EXPORT int cx_vcmp_int64(int64_t i1, int64_t i2);
 
 /**
  * Compares two integers of type unsigned int.
@@ -254,10 +230,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-int cx_cmp_uint(const void *i1, const void *i2);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT int cx_cmp_uint(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type unsigned int.
@@ -269,8 +243,7 @@
  * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
-cx_attr_export
-int cx_vcmp_uint(unsigned int i1, unsigned int i2);
+CX_EXPORT int cx_vcmp_uint(unsigned int i1, unsigned int i2);
 
 /**
  * Compares two integers of type unsigned long int.
@@ -284,10 +257,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-int cx_cmp_ulongint(const void *i1, const void *i2);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT int cx_cmp_ulongint(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type unsigned long int.
@@ -299,8 +270,7 @@
  * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
-cx_attr_export
-int cx_vcmp_ulongint(unsigned long int i1, unsigned long int i2);
+CX_EXPORT int cx_vcmp_ulongint(unsigned long int i1, unsigned long int i2);
 
 /**
  * Compares two integers of type unsigned long long.
@@ -314,10 +284,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-int cx_cmp_ulonglong(const void *i1, const void *i2);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT int cx_cmp_ulonglong(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type unsigned long long.
@@ -329,8 +297,7 @@
  * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
-cx_attr_export
-int cx_vcmp_ulonglong(unsigned long long int i1, unsigned long long int i2);
+CX_EXPORT int cx_vcmp_ulonglong(unsigned long long int i1, unsigned long long int i2);
 
 /**
  * Compares two integers of type uint16_t.
@@ -344,10 +311,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-int cx_cmp_uint16(const void *i1, const void *i2);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT int cx_cmp_uint16(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type uint16_t.
@@ -359,8 +324,7 @@
  * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
-cx_attr_export
-int cx_vcmp_uint16(uint16_t i1, uint16_t i2);
+CX_EXPORT int cx_vcmp_uint16(uint16_t i1, uint16_t i2);
 
 /**
  * Compares two integers of type uint32_t.
@@ -374,10 +338,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-int cx_cmp_uint32(const void *i1, const void *i2);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT int cx_cmp_uint32(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type uint32_t.
@@ -389,8 +351,7 @@
  * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
-cx_attr_export
-int cx_vcmp_uint32(uint32_t i1, uint32_t i2);
+CX_EXPORT int cx_vcmp_uint32(uint32_t i1, uint32_t i2);
 
 /**
  * Compares two integers of type uint64_t.
@@ -404,10 +365,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-int cx_cmp_uint64(const void *i1, const void *i2);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT int cx_cmp_uint64(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type uint64_t.
@@ -419,8 +378,34 @@
  * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
-cx_attr_export
-int cx_vcmp_uint64(uint64_t i1, uint64_t i2);
+CX_EXPORT int cx_vcmp_uint64(uint64_t i1, uint64_t i2);
+
+/**
+ * Compares two integers of type size_t.
+ *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
+ * @param i1 pointer to size_t one
+ * @param i2 pointer to size_t two
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
+ */
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT int cx_cmp_size(const void *i1, const void *i2);
+
+/**
+ * Compares two integers of type size_t.
+ *
+ * @param i1 size_t one
+ * @param i2 size_t two
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
+ */
+cx_attr_nodiscard
+CX_EXPORT int cx_vcmp_size(size_t i1, size_t i2);
 
 /**
  * Compares two real numbers of type float with precision 1e-6f.
@@ -434,10 +419,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-int cx_cmp_float(const void *f1, const void *f2);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT int cx_cmp_float(const void *f1, const void *f2);
 
 /**
  * Compares two real numbers of type float with precision 1e-6f.
@@ -449,8 +432,7 @@
  * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
-cx_attr_export
-int cx_vcmp_float(float f1, float f2);
+CX_EXPORT int cx_vcmp_float(float f1, float f2);
 
 /**
  * Compares two real numbers of type double with precision 1e-14.
@@ -464,10 +446,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-int cx_cmp_double(const void *d1, const void *d2);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT int cx_cmp_double(const void *d1, const void *d2);
 
 /**
  * Compares two real numbers of type double with precision 1e-14.
@@ -479,8 +459,7 @@
  * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
-cx_attr_export
-int cx_vcmp_double(double d1, double d2);
+CX_EXPORT int cx_vcmp_double(double d1, double d2);
 
 /**
  * Compares the integer representation of two pointers.
@@ -494,10 +473,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-int cx_cmp_intptr(const void *ptr1, const void *ptr2);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT int cx_cmp_intptr(const void *ptr1, const void *ptr2);
 
 /**
  * Compares the integer representation of two pointers.
@@ -509,8 +486,7 @@
  * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
-cx_attr_export
-int cx_vcmp_intptr(intptr_t ptr1, intptr_t ptr2);
+CX_EXPORT int cx_vcmp_intptr(intptr_t ptr1, intptr_t ptr2);
 
 /**
  * Compares the unsigned integer representation of two pointers.
@@ -524,10 +500,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-int cx_cmp_uintptr(const void *ptr1, const void *ptr2);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT int cx_cmp_uintptr(const void *ptr1, const void *ptr2);
 
 /**
  * Compares the unsigned integer representation of two pointers.
@@ -539,8 +513,7 @@
  * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
-cx_attr_export
-int cx_vcmp_uintptr(uintptr_t ptr1, uintptr_t ptr2);
+CX_EXPORT int cx_vcmp_uintptr(uintptr_t ptr1, uintptr_t ptr2);
 
 /**
  * Compares the pointers specified in the arguments without dereferencing.
@@ -551,10 +524,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-int cx_cmp_ptr(const void *ptr1, const void *ptr2);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT int cx_cmp_ptr(const void *ptr1, const void *ptr2);
 
 #ifdef __cplusplus
 } // extern "C"
--- a/ucx/cx/hash_key.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/cx/hash_key.h	Sat Nov 08 23:06:11 2025 +0100
@@ -46,14 +46,17 @@
 
 /** Internal structure for a key within a hash map. */
 struct cx_hash_key_s {
-    /** The key data. */
+    /**
+     * The key data.
+     * May be NULL when the hash is collision-free.
+     */
     const void *data;
     /**
      * The key data length.
      */
     size_t len;
     /** The hash value of the key data. */
-    unsigned hash;
+    uint64_t hash;
 };
 
 /**
@@ -76,8 +79,45 @@
  * @see cx_hash_key()
  */
 cx_attr_nonnull
-cx_attr_export
-void cx_hash_murmur(CxHashKey *key);
+CX_EXPORT void cx_hash_murmur(CxHashKey *key);
+
+/**
+ * Mixes up a 32-bit integer to be used as a hash.
+ *
+ * This function produces no collisions and has a good statistical distribution.
+ *
+ * @param x the integer
+ * @return the hash
+ */
+CX_EXPORT uint32_t cx_hash_u32(uint32_t x);
+
+/**
+ * Mixes up a 64-bit integer to be used as a hash.
+ *
+ * This function produces no collisions and has a good statistical distribution.
+ *
+ * @param x the integer
+ * @return the hash
+ */
+CX_EXPORT uint64_t cx_hash_u64(uint64_t x);
+
+/**
+ * Computes a hash key from a 32-bit integer.
+ *
+ * @param x the integer
+ * @return the hash key
+ */
+cx_attr_nodiscard
+CX_EXPORT CxHashKey cx_hash_key_u32(uint32_t x);
+
+/**
+ * Computes a hash key from a 64-bit integer.
+ *
+ * @param x the integer
+ * @return the hash key
+ */
+cx_attr_nodiscard
+CX_EXPORT CxHashKey cx_hash_key_u64(uint64_t x);
 
 /**
  * Computes a hash key from a string.
@@ -87,10 +127,22 @@
  * @param str the string
  * @return the hash key
  */
-cx_attr_nodiscard
-cx_attr_cstr_arg(1)
-cx_attr_export
-CxHashKey cx_hash_key_str(const char *str);
+cx_attr_nodiscard cx_attr_cstr_arg(1)
+CX_EXPORT CxHashKey cx_hash_key_str(const char *str);
+
+/**
+ * Computes a hash key from a string.
+ *
+ * Use this function when the string is represented
+ * as an unsigned char array.
+ *
+ * The string needs to be zero-terminated.
+ *
+ * @param str the string
+ * @return the hash key
+ */
+cx_attr_nodiscard cx_attr_cstr_arg(1)
+CX_EXPORT CxHashKey cx_hash_key_ustr(const unsigned char *str);
 
 /**
  * Computes a hash key from a byte array.
@@ -99,13 +151,8 @@
  * @param len the length
  * @return the hash key
  */
-cx_attr_nodiscard
-cx_attr_access_r(1, 2)
-cx_attr_export
-CxHashKey cx_hash_key_bytes(
-        const unsigned char *bytes,
-        size_t len
-);
+cx_attr_nodiscard cx_attr_access_r(1, 2)
+CX_EXPORT CxHashKey cx_hash_key_bytes(const unsigned char *bytes, size_t len);
 
 /**
  * Computes a hash key for an arbitrary object.
@@ -115,16 +162,21 @@
  * used for data exchange with different machines.
  *
  * @param obj a pointer to an arbitrary object
- * @param len the length of object in memory
+ * @param len the length of the object in memory
  * @return the hash key
  */
 cx_attr_nodiscard
 cx_attr_access_r(1, 2)
-cx_attr_export
-CxHashKey cx_hash_key(
-        const void *obj,
-        size_t len
-);
+CX_EXPORT CxHashKey cx_hash_key(const void *obj, size_t len);
+
+/**
+ * Computes a hash key from a UCX string.
+ *
+ * @param str the string
+ * @return the hash key
+ */
+cx_attr_nodiscard
+CX_EXPORT CxHashKey cx_hash_key_cxstr(cxstring str);
 
 /**
  * Computes a hash key from a UCX string.
@@ -133,20 +185,91 @@
  * @return the hash key
  */
 cx_attr_nodiscard
-static inline CxHashKey cx_hash_key_cxstr(cxstring str) {
-    return cx_hash_key(str.ptr, str.length);
-}
+CX_EXPORT CxHashKey cx_hash_key_mutstr(cxmutstr str);
 
 /**
- * Computes a hash key from a UCX string.
+ * The identity function for the CX_HASH_KEY() macro.
+ * You should never need to use this manually.
+ *
+ * @param key the key
+ * @return a copy of the key
+ */
+cx_attr_nodiscard
+CX_INLINE CxHashKey cx_hash_key_identity(CxHashKey key) {
+    return key;
+}
+
+#ifndef __cplusplus
+/**
+ * Creates a hash key from any of the supported types with implicit length.
+ *
+ * Does nothing when passing a CxHashkey.
+ *
+ * Supported types are UCX strings, zero-terminated C strings,
+ * and 32-bit or 64-bit unsigned integers.
  *
- * @param str (@c cxstring or @c cxmutstr) the string
- * @return (@c CxHashKey) the hash key
+ * @param key the key data
+ * @returns the @c CxHashKey
  */
-#define cx_hash_key_cxstr(str) cx_hash_key_cxstr(cx_strcast(str))
+#define CX_HASH_KEY(key) _Generic((key), \
+        CxHashKey: cx_hash_key_identity, \
+        cxstring: cx_hash_key_cxstr, \
+        cxmutstr: cx_hash_key_mutstr, \
+        char*: cx_hash_key_str, \
+        const char*: cx_hash_key_str, \
+        unsigned char*: cx_hash_key_ustr, \
+        const unsigned char*: cx_hash_key_ustr, \
+        uint32_t: cx_hash_key_u32, \
+        uint64_t: cx_hash_key_u64) \
+        (key)
+#endif // __cplusplus
+
+/**
+ * Compare function for hash keys.
+ *
+ * The pointers are untyped to be compatible with the cx_compare_func signature.
+ *
+ * @param left (@c CxHashKey*) the first key
+ * @param right (@c CxHashKey*) the second key
+ * @return zero when the keys equal, non-zero when they differ
+ */
+cx_attr_nodiscard cx_attr_nonnull
+CX_EXPORT int cx_hash_key_cmp(const void *left, const void *right);
 
 #ifdef __cplusplus
 } // extern "C"
+
+// ----------------------------------------------------------
+// Overloads of CX_HASH_KEY (the C++ version of a _Generic)
+// ----------------------------------------------------------
+
+CX_CPPDECL CxHashKey CX_HASH_KEY(CxHashKey key) {
+    return key;
+}
+
+CX_CPPDECL CxHashKey CX_HASH_KEY(cxstring str) {
+    return cx_hash_key_cxstr(str);
+}
+
+CX_CPPDECL CxHashKey CX_HASH_KEY(cxmutstr str) {
+    return cx_hash_key_mutstr(str);
+}
+
+CX_CPPDECL CxHashKey CX_HASH_KEY(const char *str) {
+    return cx_hash_key_str(str);
+}
+
+CX_CPPDECL CxHashKey CX_HASH_KEY(const unsigned char *str) {
+    return cx_hash_key_ustr(str);
+}
+
+CX_CPPDECL CxHashKey CX_HASH_KEY(uint32_t key) {
+    return cx_hash_key_u32(key);
+}
+
+CX_CPPDECL CxHashKey CX_HASH_KEY(uint64_t key) {
+    return cx_hash_key_u64(key);
+}
 #endif
 
 #endif // UCX_HASH_KEY_H
--- a/ucx/cx/hash_map.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/cx/hash_map.h	Sat Nov 08 23:06:11 2025 +0100
@@ -73,7 +73,8 @@
  * copies of the added elements.
  *
  * @note Iterators provided by this hash map implementation provide the remove operation.
- * The index value of an iterator is incremented when the iterator advanced without removal.
+ * The index value of an iterator is incremented when the iterator advanced without
+ * removing an entry.
  * In other words, when the iterator is finished, @c index==size .
  *
  * @param allocator the allocator to use
@@ -82,15 +83,9 @@
  * @param buckets the initial number of buckets in this hash map
  * @return a pointer to the new hash map
  */
-cx_attr_nodiscard
-cx_attr_malloc
-cx_attr_dealloc(cxMapFree, 1)
-cx_attr_export
-CxMap *cxHashMapCreate(
-        const CxAllocator *allocator,
-        size_t itemsize,
-        size_t buckets
-);
+cx_attr_nodiscard cx_attr_malloc cx_attr_dealloc(cxMapFree, 1)
+CX_EXPORT CxMap *cxHashMapCreate(const CxAllocator *allocator,
+        size_t itemsize, size_t buckets);
 
 /**
  * Creates a new hash map with a default number of buckets.
@@ -99,7 +94,8 @@
  * copies of the added elements.
  *
  * @note Iterators provided by this hash map implementation provide the remove operation.
- * The index value of an iterator is incremented when the iterator advanced without removal.
+ * The index value of an iterator is incremented when the iterator advanced without
+ * removing an entry.
  * In other words, when the iterator is finished, @c index==size .
  *
  * @param itemsize (@c size_t) the size of one element
@@ -111,10 +107,10 @@
  * Increases the number of buckets, if necessary.
  *
  * The load threshold is @c 0.75*buckets. If the element count exceeds the load
- * threshold, the map will be rehashed. Otherwise, no action is performed and
+ * threshold, the map will be rehashed. Otherwise, no action is performed, and
  * this function simply returns 0.
  *
- * The rehashing process ensures, that the number of buckets is at least
+ * The rehashing process ensures that the number of buckets is at least
  * 2.5 times the element count. So there is enough room for additional
  * elements without the need of another soon rehashing.
  *
@@ -127,8 +123,7 @@
  * @retval non-zero if a memory allocation error occurred
  */
 cx_attr_nonnull
-cx_attr_export
-int cxMapRehash(CxMap *map);
+CX_EXPORT int cxMapRehash(CxMap *map);
 
 
 #ifdef __cplusplus
--- a/ucx/cx/iterator.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/cx/iterator.h	Sat Nov 08 23:06:11 2025 +0100
@@ -50,7 +50,10 @@
      * True if the iterator points to valid data.
      */
     bool (*valid)(const void *);
-
+    /**
+     * Original implementation in case the function needs to be wrapped.
+     */
+    bool (*valid_impl)(const void *);
     /**
      * Returns a pointer to the current element.
      *
@@ -62,7 +65,6 @@
      * Original implementation in case the function needs to be wrapped.
      */
     void *(*current_impl)(const void *);
-
     /**
      * Advances the iterator.
      *
@@ -70,9 +72,13 @@
      */
     void (*next)(void *);
     /**
+     * Original implementation in case the function needs to be wrapped.
+     */
+    void (*next_impl)(void *);
+    /**
      * Indicates whether this iterator may remove elements.
      */
-    bool mutating;
+    bool allow_remove;
     /**
      * Internal flag for removing the current element when advancing.
      */
@@ -108,16 +114,7 @@
     /**
      * Handle for the source collection, if any.
      */
-    union {
-        /**
-         * Access for mutating iterators.
-         */
-        void *m;
-        /**
-         * Access for normal iterators.
-         */
-        const void *c;
-    } src_handle;
+    void *src_handle;
 
     /**
      * If the iterator is position-aware, contains the index of the element in the underlying collection.
@@ -141,11 +138,11 @@
  * Iterator type.
  *
  * An iterator points to a certain element in a (possibly unbounded) chain of elements.
- * Iterators that are based on collections (which have a defined "first" element), are supposed
+ * Iterators that are based on collections (which have a defined "first" element) are supposed
  * to be "position-aware", which means that they keep track of the current index within the collection.
  *
  * @note Objects that are pointed to by an iterator are always mutable through that iterator. However,
- * any concurrent mutation of the collection other than by this iterator makes this iterator invalid,
+ * any concurrent mutation of the collection other than by this iterator makes this iterator obsolete,
  * and it must not be used anymore.
  */
 typedef struct cx_iterator_s CxIterator;
@@ -178,13 +175,12 @@
 #define cxIteratorNext(iter) (iter).base.next(&iter)
 
 /**
- * Flags the current element for removal, if this iterator is mutating.
- *
- * Does nothing for non-mutating iterators.
+ * Flags the current element for removal if the iterator allows it.
  *
  * @param iter the iterator
+ * @return @c true if removal is allowed, @c false otherwise
  */
-#define cxIteratorFlagRemoval(iter) (iter).base.remove |= (iter).base.mutating
+#define cxIteratorFlagRemoval(iter) ((iter).base.remove = (iter).base.allow_remove)
 
 /**
  * Obtains a reference to an arbitrary iterator.
@@ -210,7 +206,7 @@
 /**
  * Creates an iterator for the specified plain array.
  *
- * The @p array can be @c NULL in which case the iterator will be immediately
+ * The @p array can be @c NULL, in which case the iterator will be immediately
  * initialized such that #cxIteratorValid() returns @c false.
  *
  * This iterator yields the addresses of the array elements.
@@ -218,23 +214,6 @@
  * use cxIteratorPtr() to create an iterator which directly
  * yields the stored pointers.
  *
- * @param array a pointer to the array (can be @c NULL)
- * @param elem_size the size of one array element
- * @param elem_count the number of elements in the array
- * @return an iterator for the specified array
- * @see cxIteratorPtr()
- */
-cx_attr_nodiscard
-cx_attr_export
-CxIterator cxIterator(
-        const void *array,
-        size_t elem_size,
-        size_t elem_count
-);
-
-/**
- * Creates a mutating iterator for the specified plain array.
- *
  * While the iterator is in use, the array may only be altered by removing
  * elements through #cxIteratorFlagRemoval(). Every other change to the array
  * will bring this iterator to an undefined state.
@@ -244,67 +223,45 @@
  * moving all subsequent elements by one. Usually, when the order of elements is
  * not important, this parameter should be set to @c false.
  *
- * The @p array can be @c NULL in which case the iterator will be immediately
- * initialized such that #cxIteratorValid() returns @c false.
- *
- *
  * @param array a pointer to the array (can be @c NULL)
  * @param elem_size the size of one array element
  * @param elem_count the number of elements in the array
  * @param remove_keeps_order @c true if the order of elements must be preserved
  * when removing an element
  * @return an iterator for the specified array
+ * @see cxIteratorPtr()
  */
 cx_attr_nodiscard
-cx_attr_export
-CxIterator cxMutIterator(
-        void *array,
-        size_t elem_size,
-        size_t elem_count,
-        bool remove_keeps_order
-);
+CX_EXPORT CxIterator cxIterator(const void *array,
+        size_t elem_size, size_t elem_count, bool remove_keeps_order);
 
 /**
  * Creates an iterator for the specified plain pointer array.
  *
  * This iterator assumes that every element in the array is a pointer
- * and yields exactly those pointers during iteration (while in contrast
- * an iterator created with cxIterator() would return the addresses
- * of those pointers within the array).
+ * and yields exactly those pointers during iteration (on the other
+ * hand, an iterator created with cxIterator() would return the
+ * addresses of those pointers within the array).
  *
- * @param array a pointer to the array (can be @c NULL)
- * @param elem_count the number of elements in the array
- * @return an iterator for the specified array
- * @see cxIterator()
- */
-cx_attr_nodiscard
-cx_attr_export
-CxIterator cxIteratorPtr(
-        const void *array,
-        size_t elem_count
-);
-
-/**
- * Creates a mutating iterator for the specified plain pointer array.
+ * While the iterator is in use, the array may only be altered by removing
+ * elements through #cxIteratorFlagRemoval(). Every other change to the array
+ * will bring this iterator to an undefined state.
  *
- * This is the mutating variant of cxIteratorPtr(). See also
- * cxMutIterator().
+ * When @p remove_keeps_order is set to @c false, removing an element will only
+ * move the last element to the position of the removed element, instead of
+ * moving all subsequent elements by one. Usually, when the order of elements is
+ * not important, this parameter should be set to @c false.
  *
  * @param array a pointer to the array (can be @c NULL)
  * @param elem_count the number of elements in the array
  * @param remove_keeps_order @c true if the order of elements must be preserved
  * when removing an element
  * @return an iterator for the specified array
- * @see cxMutIterator()
- * @see cxIteratorPtr()
+ * @see cxIterator()
  */
 cx_attr_nodiscard
-cx_attr_export
-CxIterator cxMutIteratorPtr(
-        void *array,
-        size_t elem_count,
-        bool remove_keeps_order
-);
+CX_EXPORT CxIterator cxIteratorPtr(const void *array, size_t elem_count,
+        bool remove_keeps_order);
 
 #ifdef __cplusplus
 } // extern "C"
--- a/ucx/cx/json.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/cx/json.h	Sat Nov 08 23:06:11 2025 +0100
@@ -90,11 +90,11 @@
      */
     CX_JSON_TOKEN_STRING,
     /**
-     * A number token that can be represented as integer.
+     * A number token that can be represented as an integer.
      */
     CX_JSON_TOKEN_INTEGER,
     /**
-     * A number token that cannot be represented as integer.
+     * A number token that cannot be represented as an integer.
      */
     CX_JSON_TOKEN_NUMBER,
     /**
@@ -196,11 +196,11 @@
  */
 typedef struct cx_mutstr_s CxJsonString;
 /**
- * Type alias for a number that can be represented as 64-bit signed integer.
+ * Type alias for a number that can be represented as a 64-bit signed integer.
  */
 typedef int64_t CxJsonInteger;
 /**
- * Type alias for number that is not an integer.
+ * Type alias for a number that is not an integer.
  */
 typedef double CxJsonNumber;
 /**
@@ -272,27 +272,27 @@
      */
     union {
         /**
-         * The array data if type is #CX_JSON_ARRAY.
+         * The array data if the type is #CX_JSON_ARRAY.
          */
         CxJsonArray array;
         /**
-         * The object data if type is #CX_JSON_OBJECT.
+         * The object data if the type is #CX_JSON_OBJECT.
          */
         CxJsonObject object;
         /**
-         * The string data if type is #CX_JSON_STRING.
+         * The string data if the type is #CX_JSON_STRING.
          */
         CxJsonString string;
         /**
-         * The integer if type is #CX_JSON_INTEGER.
+         * The integer if the type is #CX_JSON_INTEGER.
          */
         CxJsonInteger integer;
         /**
-         * The number if type is #CX_JSON_NUMBER.
+         * The number if the type is #CX_JSON_NUMBER.
          */
         CxJsonNumber number;
         /**
-         * The literal type if type is #CX_JSON_LITERAL.
+         * The literal type if the type is #CX_JSON_LITERAL.
          */
         CxJsonLiteral literal;
     } value;
@@ -377,7 +377,7 @@
 };
 
 /**
- * Status codes for the json interface.
+ * Status codes for the JSON interface.
  */
 enum cx_json_status {
     /**
@@ -391,7 +391,7 @@
     /**
      * The input ends unexpectedly.
      *
-     * Refill the buffer with cxJsonFill() to complete the json data.
+     * Refill the buffer with cxJsonFill() to complete the JSON data.
      */
     CX_JSON_INCOMPLETE_DATA,
     /**
@@ -400,7 +400,7 @@
      * You can use this enumerator to check for all "good" status results
      * by checking if the status is less than @c CX_JSON_OK.
      *
-     * A "good" status means, that you can refill data and continue parsing.
+     * A "good" status means that you can refill data and continue parsing.
      */
     CX_JSON_OK,
     /**
@@ -412,7 +412,7 @@
      */
     CX_JSON_BUFFER_ALLOC_FAILED,
     /**
-     * Allocating memory for a json value failed.
+     * Allocating memory for a JSON value failed.
      */
     CX_JSON_VALUE_ALLOC_FAILED,
     /**
@@ -426,7 +426,7 @@
 };
 
 /**
- * Typedef for the json status enum.
+ * Typedef for the JSON status enum.
  */
 typedef enum cx_json_status CxJsonStatus;
 
@@ -445,7 +445,7 @@
     /**
      * The maximum number of fractional digits in a number value.
      * The default value is 6 and values larger than 15 are reduced to 15.
-     * Note, that the actual number of digits may be lower, depending on the concrete number.
+     * Note that the actual number of digits may be lower, depending on the concrete number.
      */
     uint8_t frac_max_digits;
     /**
@@ -465,7 +465,7 @@
 };
 
 /**
- * Typedef for the json writer.
+ * Typedef for the JSON writer.
  */
 typedef struct cx_json_writer_s CxJsonWriter;
 
@@ -475,8 +475,7 @@
  * @return new JSON writer settings
  */
 cx_attr_nodiscard
-cx_attr_export
-CxJsonWriter cxJsonWriterCompact(void);
+CX_EXPORT CxJsonWriter cxJsonWriterCompact(void);
 
 /**
  * Creates a default writer configuration for pretty output.
@@ -485,8 +484,7 @@
  * @return new JSON writer settings
  */
 cx_attr_nodiscard
-cx_attr_export
-CxJsonWriter cxJsonWriterPretty(bool use_spaces);
+CX_EXPORT CxJsonWriter cxJsonWriterPretty(bool use_spaces);
 
 /**
  * Writes a JSON value to a buffer or stream.
@@ -496,9 +494,8 @@
  * that the data is only partially written when an error occurs with no
  * way to indicate how much data was written.
  * To avoid this problem, you can use a CxBuffer as @p target which is
- * unlikely to fail a write operation and either use the buffer's flush
- * feature to relay the data or use the data in the buffer manually to
- * write it to the actual target.
+ * unlikely to fail a write operation. You can, for example, use the buffer's flush
+ * feature to relay the data.
  *
  * @param target the buffer or stream where to write to
  * @param value the value that shall be written
@@ -508,49 +505,38 @@
  * @retval non-zero when no or not all data could be written
  */
 cx_attr_nonnull_arg(1, 2, 3)
-cx_attr_export
-int cxJsonWrite(
-    void* target,
-    const CxJsonValue* value,
-    cx_write_func wfunc,
-    const CxJsonWriter* settings
-);
+CX_EXPORT int cxJsonWrite(void* target, const CxJsonValue* value,
+        cx_write_func wfunc, const CxJsonWriter* settings);
 
 /**
- * Initializes the json interface.
+ * Initializes the JSON interface.
  *
- * @param json the json interface
+ * @param json the JSON interface
  * @param allocator the allocator that shall be used for the produced values
  * @see cxJsonDestroy()
  */
 cx_attr_nonnull_arg(1)
-cx_attr_export
-void cxJsonInit(CxJson *json, const CxAllocator *allocator);
+CX_EXPORT void cxJsonInit(CxJson *json, const CxAllocator *allocator);
 
 /**
- * Destroys the json interface.
+ * Destroys the JSON interface.
  *
- * @param json the json interface
+ * @param json the JSON interface
  * @see cxJsonInit()
  */
 cx_attr_nonnull
-cx_attr_export
-void cxJsonDestroy(CxJson *json);
+CX_EXPORT void cxJsonDestroy(CxJson *json);
 
 /**
- * Destroys and re-initializes the json interface.
+ * Destroys and re-initializes the JSON interface.
  *
- * You might want to use this, to reset the parser after
+ * You might want to use this to reset the parser after
  * encountering a syntax error.
  *
- * @param json the json interface
+ * @param json the JSON interface
  */
 cx_attr_nonnull
-static inline void cxJsonReset(CxJson *json) {
-    const CxAllocator *allocator = json->allocator;
-    cxJsonDestroy(json);
-    cxJsonInit(json, allocator);
-}
+CX_EXPORT void cxJsonReset(CxJson *json);
 
 /**
  * Fills the input buffer.
@@ -563,48 +549,30 @@
  * the additional data is appended - inevitably leading to
  * an allocation of a new buffer and copying the previous contents.
  *
- * @param json the json interface
+ * @param json the JSON interface
  * @param buf the source buffer
  * @param len the length of the source buffer
  * @retval zero success
  * @retval non-zero internal allocation error
  * @see cxJsonFill()
  */
-cx_attr_nonnull
-cx_attr_access_r(2, 3)
-cx_attr_export
-int cxJsonFilln(CxJson *json, const char *buf, size_t len);
+cx_attr_nonnull cx_attr_access_r(2, 3)
+CX_EXPORT int cxJsonFilln(CxJson *json, const char *buf, size_t len);
+
 
-#ifdef __cplusplus
-} // extern "C"
-
+/**
+ * Internal function, do not use.
+ *
+ * @param json the JSON interface
+ * @param str the string
+ * @retval zero success
+ * @retval non-zero internal allocation error
+ */
 cx_attr_nonnull
-static inline int cxJsonFill(
-        CxJson *json,
-        cxstring str
-) {
+CX_INLINE int cx_json_fill(CxJson *json, cxstring str) {
     return cxJsonFilln(json, str.ptr, str.length);
 }
 
-cx_attr_nonnull
-static inline int cxJsonFill(
-        CxJson *json,
-        cxmutstr str
-) {
-    return cxJsonFilln(json, str.ptr, str.length);
-}
-
-cx_attr_nonnull
-cx_attr_cstr_arg(2)
-static inline int cxJsonFill(
-        CxJson *json,
-        const char *str
-) {
-    return cxJsonFilln(json, str, strlen(str));
-}
-
-extern "C" {
-#else // __cplusplus
 /**
  * Fills the input buffer.
  *
@@ -616,53 +584,13 @@
  * the additional data is appended - inevitably leading to
  * an allocation of a new buffer and copying the previous contents.
  *
- * @param json the json interface
+ * @param json the JSON interface
  * @param str the source string
  * @retval zero success
  * @retval non-zero internal allocation error
  * @see cxJsonFilln()
  */
-#define cxJsonFill(json, str) _Generic((str), \
-    cxstring: cx_json_fill_cxstr,             \
-    cxmutstr: cx_json_fill_mutstr,            \
-    char*: cx_json_fill_str,                  \
-    const char*: cx_json_fill_str)            \
-    (json, str)
-
-/**
- * @copydoc cxJsonFill()
- */
-cx_attr_nonnull
-static inline int cx_json_fill_cxstr(
-        CxJson *json,
-        cxstring str
-) {
-    return cxJsonFilln(json, str.ptr, str.length);
-}
-
-/**
- * @copydoc cxJsonFill()
- */
-cx_attr_nonnull
-static inline int cx_json_fill_mutstr(
-        CxJson *json,
-        cxmutstr str
-) {
-    return cxJsonFilln(json, str.ptr, str.length);
-}
-
-/**
- * @copydoc cxJsonFill()
- */
-cx_attr_nonnull
-cx_attr_cstr_arg(2)
-static inline int cx_json_fill_str(
-        CxJson *json,
-        const char *str
-) {
-    return cxJsonFilln(json, str, strlen(str));
-}
-#endif
+#define cxJsonFill(json, str) cx_json_fill(json, cx_strcast(str))
 
 /**
  * Creates a new (empty) JSON object.
@@ -673,8 +601,7 @@
  * @see cxJsonArrAddValues()
  */
 cx_attr_nodiscard
-cx_attr_export
-CxJsonValue* cxJsonCreateObj(const CxAllocator* allocator);
+CX_EXPORT CxJsonValue* cxJsonCreateObj(const CxAllocator* allocator);
 
 /**
  * Creates a new (empty) JSON array.
@@ -685,8 +612,7 @@
  * @see cxJsonArrAddValues()
  */
 cx_attr_nodiscard
-cx_attr_export
-CxJsonValue* cxJsonCreateArr(const CxAllocator* allocator);
+CX_EXPORT CxJsonValue* cxJsonCreateArr(const CxAllocator* allocator);
 
 /**
  * Creates a new JSON number value.
@@ -698,8 +624,7 @@
  * @see cxJsonArrAddNumbers()
  */
 cx_attr_nodiscard
-cx_attr_export
-CxJsonValue* cxJsonCreateNumber(const CxAllocator* allocator, double num);
+CX_EXPORT CxJsonValue* cxJsonCreateNumber(const CxAllocator* allocator, double num);
 
 /**
  * Creates a new JSON number value based on an integer.
@@ -711,8 +636,7 @@
  * @see cxJsonArrAddIntegers()
  */
 cx_attr_nodiscard
-cx_attr_export
-CxJsonValue* cxJsonCreateInteger(const CxAllocator* allocator, int64_t num);
+CX_EXPORT CxJsonValue* cxJsonCreateInteger(const CxAllocator* allocator, int64_t num);
 
 /**
  * Creates a new JSON string.
@@ -724,11 +648,8 @@
  * @see cxJsonObjPutString()
  * @see cxJsonArrAddStrings()
  */
-cx_attr_nodiscard
-cx_attr_nonnull_arg(2)
-cx_attr_cstr_arg(2)
-cx_attr_export
-CxJsonValue* cxJsonCreateString(const CxAllocator* allocator, const char *str);
+cx_attr_nodiscard cx_attr_nonnull_arg(2) cx_attr_cstr_arg(2)
+CX_EXPORT CxJsonValue* cxJsonCreateString(const CxAllocator* allocator, const char *str);
 
 /**
  * Creates a new JSON string.
@@ -741,8 +662,7 @@
  * @see cxJsonArrAddCxStrings()
  */
 cx_attr_nodiscard
-cx_attr_export
-CxJsonValue* cxJsonCreateCxString(const CxAllocator* allocator, cxstring str);
+CX_EXPORT CxJsonValue* cxJsonCreateCxString(const CxAllocator* allocator, cxstring str);
 
 /**
  * Creates a new JSON literal.
@@ -754,8 +674,7 @@
  * @see cxJsonArrAddLiterals()
  */
 cx_attr_nodiscard
-cx_attr_export
-CxJsonValue* cxJsonCreateLiteral(const CxAllocator* allocator, CxJsonLiteral lit);
+CX_EXPORT CxJsonValue* cxJsonCreateLiteral(const CxAllocator* allocator, CxJsonLiteral lit);
 
 /**
  * Adds number values to a JSON array.
@@ -766,10 +685,8 @@
  * @retval zero success
  * @retval non-zero allocation failure
  */
-cx_attr_nonnull
-cx_attr_access_r(2, 3)
-cx_attr_export
-int cxJsonArrAddNumbers(CxJsonValue* arr, const double* num, size_t count);
+cx_attr_nonnull cx_attr_access_r(2, 3)
+CX_EXPORT int cxJsonArrAddNumbers(CxJsonValue* arr, const double* num, size_t count);
 
 /**
  * Adds number values, of which all are integers, to a JSON array.
@@ -780,10 +697,8 @@
  * @retval zero success
  * @retval non-zero allocation failure
  */
-cx_attr_nonnull
-cx_attr_access_r(2, 3)
-cx_attr_export
-int cxJsonArrAddIntegers(CxJsonValue* arr, const int64_t* num, size_t count);
+cx_attr_nonnull cx_attr_access_r(2, 3)
+CX_EXPORT int cxJsonArrAddIntegers(CxJsonValue* arr, const int64_t* num, size_t count);
 
 /**
  * Adds strings to a JSON array.
@@ -797,10 +712,8 @@
  * @retval non-zero allocation failure
  * @see cxJsonArrAddCxStrings()
  */
-cx_attr_nonnull
-cx_attr_access_r(2, 3)
-cx_attr_export
-int cxJsonArrAddStrings(CxJsonValue* arr, const char* const* str, size_t count);
+cx_attr_nonnull cx_attr_access_r(2, 3)
+CX_EXPORT int cxJsonArrAddStrings(CxJsonValue* arr, const char* const* str, size_t count);
 
 /**
  * Adds strings to a JSON array.
@@ -814,10 +727,8 @@
  * @retval non-zero allocation failure
  * @see cxJsonArrAddStrings()
  */
-cx_attr_nonnull
-cx_attr_access_r(2, 3)
-cx_attr_export
-int cxJsonArrAddCxStrings(CxJsonValue* arr, const cxstring* str, size_t count);
+cx_attr_nonnull cx_attr_access_r(2, 3)
+CX_EXPORT int cxJsonArrAddCxStrings(CxJsonValue* arr, const cxstring* str, size_t count);
 
 /**
  * Adds literals to a JSON array.
@@ -828,10 +739,8 @@
  * @retval zero success
  * @retval non-zero allocation failure
  */
-cx_attr_nonnull
-cx_attr_access_r(2, 3)
-cx_attr_export
-int cxJsonArrAddLiterals(CxJsonValue* arr, const CxJsonLiteral* lit, size_t count);
+cx_attr_nonnull cx_attr_access_r(2, 3)
+CX_EXPORT int cxJsonArrAddLiterals(CxJsonValue* arr, const CxJsonLiteral* lit, size_t count);
 
 /**
  * Add arbitrary values to a JSON array.
@@ -845,10 +754,8 @@
  * @retval zero success
  * @retval non-zero allocation failure
  */
-cx_attr_nonnull
-cx_attr_access_r(2, 3)
-cx_attr_export
-int cxJsonArrAddValues(CxJsonValue* arr, CxJsonValue* const* val, size_t count);
+cx_attr_nonnull cx_attr_access_r(2, 3)
+CX_EXPORT int cxJsonArrAddValues(CxJsonValue* arr, CxJsonValue* const* val, size_t count);
 
 /**
  * Adds or replaces a value within a JSON object.
@@ -865,8 +772,7 @@
  * @retval non-zero allocation failure
  */
 cx_attr_nonnull
-cx_attr_export
-int cxJsonObjPut(CxJsonValue* obj, cxstring name, CxJsonValue* child);
+CX_EXPORT int cxJsonObjPut(CxJsonValue* obj, cxstring name, CxJsonValue* child);
 
 /**
  * Creates a new JSON object and adds it to an existing object.
@@ -878,8 +784,7 @@
  * @see cxJsonCreateObj()
  */
 cx_attr_nonnull
-cx_attr_export
-CxJsonValue* cxJsonObjPutObj(CxJsonValue* obj, cxstring name);
+CX_EXPORT CxJsonValue* cxJsonObjPutObj(CxJsonValue* obj, cxstring name);
 
 /**
  * Creates a new JSON array and adds it to an object.
@@ -891,8 +796,7 @@
  * @see cxJsonCreateArr()
  */
 cx_attr_nonnull
-cx_attr_export
-CxJsonValue* cxJsonObjPutArr(CxJsonValue* obj, cxstring name);
+CX_EXPORT CxJsonValue* cxJsonObjPutArr(CxJsonValue* obj, cxstring name);
 
 /**
  * Creates a new JSON number and adds it to an object.
@@ -905,8 +809,7 @@
  * @see cxJsonCreateNumber()
  */
 cx_attr_nonnull
-cx_attr_export
-CxJsonValue* cxJsonObjPutNumber(CxJsonValue* obj, cxstring name, double num);
+CX_EXPORT CxJsonValue* cxJsonObjPutNumber(CxJsonValue* obj, cxstring name, double num);
 
 /**
  * Creates a new JSON number, based on an integer, and adds it to an object.
@@ -919,8 +822,7 @@
  * @see cxJsonCreateInteger()
  */
 cx_attr_nonnull
-cx_attr_export
-CxJsonValue* cxJsonObjPutInteger(CxJsonValue* obj, cxstring name, int64_t num);
+CX_EXPORT CxJsonValue* cxJsonObjPutInteger(CxJsonValue* obj, cxstring name, int64_t num);
 
 /**
  * Creates a new JSON string and adds it to an object.
@@ -934,10 +836,8 @@
  * @see cxJsonObjPut()
  * @see cxJsonCreateString()
  */
-cx_attr_nonnull
-cx_attr_cstr_arg(3)
-cx_attr_export
-CxJsonValue* cxJsonObjPutString(CxJsonValue* obj, cxstring name, const char* str);
+cx_attr_nonnull cx_attr_cstr_arg(3)
+CX_EXPORT CxJsonValue* cxJsonObjPutString(CxJsonValue* obj, cxstring name, const char* str);
 
 /**
  * Creates a new JSON string and adds it to an object.
@@ -952,8 +852,7 @@
  * @see cxJsonCreateCxString()
  */
 cx_attr_nonnull
-cx_attr_export
-CxJsonValue* cxJsonObjPutCxString(CxJsonValue* obj, cxstring name, cxstring str);
+CX_EXPORT CxJsonValue* cxJsonObjPutCxString(CxJsonValue* obj, cxstring name, cxstring str);
 
 /**
  * Creates a new JSON literal and adds it to an object.
@@ -966,22 +865,20 @@
  * @see cxJsonCreateLiteral()
  */
 cx_attr_nonnull
-cx_attr_export
-CxJsonValue* cxJsonObjPutLiteral(CxJsonValue* obj, cxstring name, CxJsonLiteral lit);
+CX_EXPORT CxJsonValue* cxJsonObjPutLiteral(CxJsonValue* obj, cxstring name, CxJsonLiteral lit);
 
 /**
  * Recursively deallocates the memory of a JSON value.
  *
  * @remark The type of each deallocated value will be changed
- * to #CX_JSON_NOTHING and values of such type will be skipped
- * by the de-allocation. That means, this function protects
+ * to #CX_JSON_NOTHING, and values of such a type will be skipped
+ * by the deallocation. That means this function protects
  * you from double-frees when you are accidentally freeing
  * a nested value and then the parent value (or vice versa).
  *
  * @param value the value
  */
-cx_attr_export
-void cxJsonValueFree(CxJsonValue *value);
+CX_EXPORT void cxJsonValueFree(CxJsonValue *value);
 
 /**
  * Tries to obtain the next JSON value.
@@ -993,7 +890,7 @@
  * add the missing data with another invocation of cxJsonFill()
  * and then repeat the call to cxJsonNext().
  *
- * @param json the json interface
+ * @param json the JSON interface
  * @param value a pointer where the next value shall be stored
  * @retval CX_JSON_NO_ERROR successfully retrieve the @p value
  * @retval CX_JSON_NO_DATA there is no (more) data in the buffer to read from
@@ -1005,10 +902,8 @@
  * @retval CX_JSON_FORMAT_ERROR_NUMBER the JSON text contains an illegally formatted number
  * @retval CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN JSON syntax error
  */
-cx_attr_nonnull
-cx_attr_access_w(2)
-cx_attr_export
-CxJsonStatus cxJsonNext(CxJson *json, CxJsonValue **value);
+cx_attr_nonnull cx_attr_access_w(2)
+CX_EXPORT CxJsonStatus cxJsonNext(CxJson *json, CxJsonValue **value);
 
 /**
  * Checks if the specified value is a JSON object.
@@ -1018,7 +913,7 @@
  * @retval false otherwise
  */
 cx_attr_nonnull
-static inline bool cxJsonIsObject(const CxJsonValue *value) {
+CX_INLINE bool cxJsonIsObject(const CxJsonValue *value) {
     return value->type == CX_JSON_OBJECT;
 }
 
@@ -1030,7 +925,7 @@
  * @retval false otherwise
  */
 cx_attr_nonnull
-static inline bool cxJsonIsArray(const CxJsonValue *value) {
+CX_INLINE bool cxJsonIsArray(const CxJsonValue *value) {
     return value->type == CX_JSON_ARRAY;
 }
 
@@ -1042,14 +937,14 @@
  * @retval false otherwise
  */
 cx_attr_nonnull
-static inline bool cxJsonIsString(const CxJsonValue *value) {
+CX_INLINE bool cxJsonIsString(const CxJsonValue *value) {
     return value->type == CX_JSON_STRING;
 }
 
 /**
  * Checks if the specified value is a JSON number.
  *
- * This function will return true for both floating point and
+ * This function will return true for both floating-point and
  * integer numbers.
  *
  * @param value a pointer to the value
@@ -1058,7 +953,7 @@
  * @see cxJsonIsInteger()
  */
 cx_attr_nonnull
-static inline bool cxJsonIsNumber(const CxJsonValue *value) {
+CX_INLINE bool cxJsonIsNumber(const CxJsonValue *value) {
     return value->type == CX_JSON_NUMBER || value->type == CX_JSON_INTEGER;
 }
 
@@ -1071,7 +966,7 @@
  * @see cxJsonIsNumber()
  */
 cx_attr_nonnull
-static inline bool cxJsonIsInteger(const CxJsonValue *value) {
+CX_INLINE bool cxJsonIsInteger(const CxJsonValue *value) {
     return value->type == CX_JSON_INTEGER;
 }
 
@@ -1088,7 +983,7 @@
  * @see cxJsonIsNull()
  */
 cx_attr_nonnull
-static inline bool cxJsonIsLiteral(const CxJsonValue *value) {
+CX_INLINE bool cxJsonIsLiteral(const CxJsonValue *value) {
     return value->type == CX_JSON_LITERAL;
 }
 
@@ -1102,14 +997,14 @@
  * @see cxJsonIsFalse()
  */
 cx_attr_nonnull
-static inline bool cxJsonIsBool(const CxJsonValue *value) {
+CX_INLINE bool cxJsonIsBool(const CxJsonValue *value) {
     return cxJsonIsLiteral(value) && value->value.literal != CX_JSON_NULL;
 }
 
 /**
  * Checks if the specified value is @c true.
  *
- * @remark Be advised, that this is not the same as
+ * @remark Be advised that this is different from
  * testing @c !cxJsonIsFalse(v).
  *
  * @param value a pointer to the value
@@ -1119,14 +1014,14 @@
  * @see cxJsonIsFalse()
  */
 cx_attr_nonnull
-static inline bool cxJsonIsTrue(const CxJsonValue *value) {
+CX_INLINE bool cxJsonIsTrue(const CxJsonValue *value) {
     return cxJsonIsLiteral(value) && value->value.literal == CX_JSON_TRUE;
 }
 
 /**
  * Checks if the specified value is @c false.
  *
- * @remark Be advised, that this is not the same as
+ * @remark Be advised that this is different from
  * testing @c !cxJsonIsTrue(v).
  *
  * @param value a pointer to the value
@@ -1136,7 +1031,7 @@
  * @see cxJsonIsTrue()
  */
 cx_attr_nonnull
-static inline bool cxJsonIsFalse(const CxJsonValue *value) {
+CX_INLINE bool cxJsonIsFalse(const CxJsonValue *value) {
     return cxJsonIsLiteral(value) && value->value.literal == CX_JSON_FALSE;
 }
 
@@ -1149,7 +1044,7 @@
  * @see cxJsonIsLiteral()
  */
 cx_attr_nonnull
-static inline bool cxJsonIsNull(const CxJsonValue *value) {
+CX_INLINE bool cxJsonIsNull(const CxJsonValue *value) {
     return cxJsonIsLiteral(value) && value->value.literal == CX_JSON_NULL;
 }
 
@@ -1162,11 +1057,8 @@
  * @return the value represented as C string
  * @see cxJsonIsString()
  */
-cx_attr_nonnull
-cx_attr_returns_nonnull
-static inline char *cxJsonAsString(const CxJsonValue *value) {
-    return value->value.string.ptr;
-}
+cx_attr_nonnull  cx_attr_returns_nonnull
+CX_EXPORT char *cxJsonAsString(const CxJsonValue *value);
 
 /**
  * Obtains a UCX string from the given JSON value.
@@ -1178,9 +1070,7 @@
  * @see cxJsonIsString()
  */
 cx_attr_nonnull
-static inline cxstring cxJsonAsCxString(const CxJsonValue *value) {
-    return cx_strcast(value->value.string);
-}
+CX_EXPORT cxstring cxJsonAsCxString(const CxJsonValue *value);
 
 /**
  * Obtains a mutable UCX string from the given JSON value.
@@ -1192,12 +1082,10 @@
  * @see cxJsonIsString()
  */
 cx_attr_nonnull
-static inline cxmutstr cxJsonAsCxMutStr(const CxJsonValue *value) {
-    return value->value.string;
-}
+CX_EXPORT cxmutstr cxJsonAsCxMutStr(const CxJsonValue *value);
 
 /**
- * Obtains a double-precision floating point value from the given JSON value.
+ * Obtains a double-precision floating-point value from the given JSON value.
  *
  * If the @p value is not a JSON number, the behavior is undefined.
  *
@@ -1206,13 +1094,7 @@
  * @see cxJsonIsNumber()
  */
 cx_attr_nonnull
-static inline double cxJsonAsDouble(const CxJsonValue *value) {
-    if (value->type == CX_JSON_INTEGER) {
-        return (double) value->value.integer;
-    } else {
-        return value->value.number;
-    }
-}
+CX_EXPORT double cxJsonAsDouble(const CxJsonValue *value);
 
 /**
  * Obtains a 64-bit signed integer from the given JSON value.
@@ -1227,13 +1109,7 @@
  * @see cxJsonIsInteger()
  */
 cx_attr_nonnull
-static inline int64_t cxJsonAsInteger(const CxJsonValue *value) {
-    if (value->type == CX_JSON_INTEGER) {
-        return value->value.integer;
-    } else {
-        return (int64_t) value->value.number;
-    }
-}
+CX_EXPORT int64_t cxJsonAsInteger(const CxJsonValue *value);
 
 /**
  * Obtains a Boolean value from the given JSON value.
@@ -1246,7 +1122,7 @@
  * @see cxJsonIsLiteral()
  */
 cx_attr_nonnull
-static inline bool cxJsonAsBool(const CxJsonValue *value) {
+CX_INLINE bool cxJsonAsBool(const CxJsonValue *value) {
     return value->value.literal == CX_JSON_TRUE;
 }
 
@@ -1260,7 +1136,7 @@
  * @see cxJsonIsArray()
  */
 cx_attr_nonnull
-static inline size_t cxJsonArrSize(const CxJsonValue *value) {
+CX_INLINE size_t cxJsonArrSize(const CxJsonValue *value) {
     return value->value.array.array_size;
 }
 
@@ -1278,10 +1154,8 @@
  * @return the value at the specified index
  * @see cxJsonIsArray()
  */
-cx_attr_nonnull
-cx_attr_returns_nonnull
-cx_attr_export
-CxJsonValue *cxJsonArrGet(const CxJsonValue *value, size_t index);
+cx_attr_nonnull cx_attr_returns_nonnull
+CX_EXPORT CxJsonValue *cxJsonArrGet(const CxJsonValue *value, size_t index);
 
 /**
  * Removes an element from a JSON array.
@@ -1297,8 +1171,7 @@
  * @see cxJsonIsArray()
  */
 cx_attr_nonnull
-cx_attr_export
-CxJsonValue *cxJsonArrRemove(CxJsonValue *value, size_t index);
+CX_EXPORT CxJsonValue *cxJsonArrRemove(CxJsonValue *value, size_t index);
 
 /**
  * Returns an iterator over the JSON array elements.
@@ -1311,10 +1184,8 @@
  * @return an iterator over the array elements
  * @see cxJsonIsArray()
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-CxIterator cxJsonArrIter(const CxJsonValue *value);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT CxIterator cxJsonArrIter(const CxJsonValue *value);
 
 /**
  * Returns an iterator over the JSON object members.
@@ -1328,55 +1199,18 @@
  * @return an iterator over the object members
  * @see cxJsonIsObject()
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-CxIterator cxJsonObjIter(const CxJsonValue *value);
-
-/**
- * @copydoc cxJsonObjGet()
- */
-cx_attr_nonnull
-cx_attr_returns_nonnull
-cx_attr_export
-CxJsonValue *cx_json_obj_get_cxstr(const CxJsonValue *value, cxstring name);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT CxIterator cxJsonObjIter(const CxJsonValue *value);
 
 /**
- * @copydoc cxJsonObjRemove()
+ * Internal function, do not use.
+ * @param value the JSON object
+ * @param name the key to look up
+ * @return the value corresponding to the key
  */
-cx_attr_nonnull
-cx_attr_export
-CxJsonValue *cx_json_obj_remove_cxstr(CxJsonValue *value, cxstring name);
-
-#ifdef __cplusplus
-} // extern "C"
-
-static inline CxJsonValue *cxJsonObjGet(const CxJsonValue *value, cxstring name) {
-    return cx_json_obj_get_cxstr(value, name);
-}
-
-static inline CxJsonValue *cxJsonObjGet(const CxJsonValue *value, cxmutstr name) {
-    return cx_json_obj_get_cxstr(value, cx_strcast(name));
-}
+cx_attr_nonnull cx_attr_returns_nonnull
+CX_EXPORT CxJsonValue *cx_json_obj_get(const CxJsonValue *value, cxstring name);
 
-static inline CxJsonValue *cxJsonObjGet(const CxJsonValue *value, const char *name) {
-    return cx_json_obj_get_cxstr(value, cx_str(name));
-}
-
-static inline CxJsonValue *cxJsonObjRemove(CxJsonValue *value, cxstring name) {
-    return cx_json_obj_remove_cxstr(value, name);
-}
-
-static inline CxJsonValue *cxJsonObjRemove(CxJsonValue *value, cxmutstr name) {
-    return cx_json_obj_remove_cxstr(value, cx_strcast(name));
-}
-
-static inline CxJsonValue *cxJsonObjRemove(CxJsonValue *value, const char *name) {
-    return cx_json_obj_remove_cxstr(value, cx_str(name));
-}
-
-extern "C" {
-#else
 /**
  * Returns a value corresponding to a key in a JSON object.
  *
@@ -1391,31 +1225,16 @@
  * @return the value corresponding to the key
  * @see cxJsonIsObject()
  */
-#define cxJsonObjGet(value, name) _Generic((name), \
-        cxstring: cx_json_obj_get_cxstr,           \
-        cxmutstr: cx_json_obj_get_mutstr,          \
-        char*: cx_json_obj_get_str,                \
-        const char*: cx_json_obj_get_str)          \
-        (value, name)
+#define cxJsonObjGet(value, name) cx_json_obj_get(value, cx_strcast(name))
 
 /**
- * @copydoc cxJsonObjGet()
+ * Internal function, do not use.
+ * @param value the JSON object
+ * @param name the key to look up
+ * @return the value corresponding to the key or @c NULL when the key is not part of the object
  */
 cx_attr_nonnull
-cx_attr_returns_nonnull
-static inline CxJsonValue *cx_json_obj_get_mutstr(const CxJsonValue *value, cxmutstr name) {
-    return cx_json_obj_get_cxstr(value, cx_strcast(name));
-}
-
-/**
- * @copydoc cxJsonObjGet()
- */
-cx_attr_nonnull
-cx_attr_returns_nonnull
-cx_attr_cstr_arg(2)
-static inline CxJsonValue *cx_json_obj_get_str(const CxJsonValue *value, const char *name) {
-    return cx_json_obj_get_cxstr(value, cx_str(name));
-}
+CX_EXPORT CxJsonValue *cx_json_obj_remove(CxJsonValue *value, cxstring name);
 
 /**
  * Removes and returns a value corresponding to a key in a JSON object.
@@ -1430,30 +1249,7 @@
  * @return the value corresponding to the key or @c NULL when the key is not part of the object
  * @see cxJsonIsObject()
  */
-#define cxJsonObjRemove(value, name) _Generic((name), \
-        cxstring: cx_json_obj_remove_cxstr,           \
-        cxmutstr: cx_json_obj_remove_mutstr,          \
-        char*: cx_json_obj_remove_str,                \
-        const char*: cx_json_obj_remove_str)          \
-        (value, name)
-
-/**
- * @copydoc cxJsonObjRemove()
- */
-cx_attr_nonnull
-static inline CxJsonValue *cx_json_obj_remove_mutstr(CxJsonValue *value, cxmutstr name) {
-    return cx_json_obj_remove_cxstr(value, cx_strcast(name));
-}
-
-/**
- * @copydoc cxJsonObjRemove()
- */
-cx_attr_nonnull
-cx_attr_cstr_arg(2)
-static inline CxJsonValue *cx_json_obj_remove_str(CxJsonValue *value, const char *name) {
-    return cx_json_obj_remove_cxstr(value, cx_str(name));
-}
-#endif
+#define cxJsonObjRemove(value, name) cx_json_obj_remove(value, cx_strcast(name))
 
 #ifdef __cplusplus
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ucx/cx/kv_list.h	Sat Nov 08 23:06:11 2025 +0100
@@ -0,0 +1,260 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+/**
+ * @file kv_list.h
+ * @brief Linked list implementation with key/value-lookup.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
+ */
+
+#ifndef UCX_KV_LIST_H
+#define UCX_KV_LIST_H
+
+#include "common.h"
+#include "list.h"
+#include "map.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Allocates a linked list with a lookup-map for storing elements with @p elem_size bytes each.
+ *
+ * If @p elem_size is #CX_STORE_POINTERS, the created list stores pointers instead of
+ * copies of the added elements, and the compare function will be automatically set
+ * to cx_cmp_ptr() if none is given.
+ *
+ * After creating the list, it can also be used as a map after converting the pointer
+ * to a CxMap pointer with cxKvListAsMap().
+ * When you want to use the list interface again, you can also convert the map pointer back
+ * with cxKvListAsList().
+ *
+ * @param allocator the allocator for allocating the list nodes
+ * (if @c NULL, the cxDefaultAllocator will be used)
+ * @param comparator the comparator for the elements
+ * (if @c NULL, and the list is not storing pointers, sort and find
+ * functions will not work)
+ * @param elem_size the size of each element in bytes
+ * @return the created list
+ * @see cxKvListAsMap()
+ * @see cxKvListAsList()
+ */
+cx_attr_nodiscard cx_attr_malloc cx_attr_dealloc(cxListFree, 1)
+CX_EXPORT CxList *cxKvListCreate(const CxAllocator *allocator,
+        cx_compare_func comparator, size_t elem_size);
+
+/**
+ * Allocates a linked list with a lookup-map for storing elements with @p elem_size bytes each.
+ *
+ * If @p elem_size is #CX_STORE_POINTERS, the created list stores pointers instead of
+ * copies of the added elements, and the compare function will be automatically set
+ * to cx_cmp_ptr() if none is given.
+ *
+ * This function creates the list with cxKvListCreate() and immediately applies
+ * cxKvListAsMap(). If you want to use the returned object as a list, you can call
+ * cxKvListAsList() later.
+ *
+ * @param allocator the allocator for allocating the list nodes
+ * (if @c NULL, the cxDefaultAllocator will be used)
+ * @param comparator the comparator for the elements
+ * (if @c NULL, and the list is not storing pointers, sort and find
+ * functions will not work)
+ * @param elem_size the size of each element in bytes
+ * @return the created list wrapped into the CxMap interface
+ * @see cxKvListAsMap()
+ * @see cxKvListAsList()
+ */
+cx_attr_nodiscard cx_attr_malloc cx_attr_dealloc(cxMapFree, 1)
+CX_EXPORT CxMap *cxKvListCreateAsMap(const CxAllocator *allocator,
+        cx_compare_func comparator, size_t elem_size);
+
+/**
+ * Allocates a linked list with a lookup-map for storing elements with @p elem_size bytes each.
+ *
+ * The list will use cxDefaultAllocator and no comparator function. If you want
+ * to call functions that need a comparator, you must either set one immediately
+ * after list creation or use cxKvListCreate().
+ *
+ * If @p elem_size is #CX_STORE_POINTERS, the created list stores pointers instead of
+ * copies of the added elements, and the compare function will be automatically set
+ * to cx_cmp_ptr().
+ *
+ * After creating the list, it can also be used as a map after converting the pointer
+ * to a CxMap pointer with cxKvListAsMap().
+ * When you want to use the list interface again, you can also convert the map pointer back
+ * with cxKvListAsList().
+ *
+ * @param elem_size (@c size_t) the size of each element in bytes
+ * @return (@c CxList*) the created list
+ * @see cxKvListAsMap()
+ * @see cxKvListAsList()
+ */
+#define cxKvListCreateSimple(elem_size) cxKvListCreate(NULL, NULL, elem_size)
+
+/**
+ * Allocates a linked list with a lookup-map for storing elements with @p elem_size bytes each.
+ *
+ * The list will use cxDefaultAllocator and no comparator function. If you want
+ * to call functions that need a comparator, you must either set one immediately
+ * after list creation or use cxKvListCreate().
+ *
+ * If @p elem_size is #CX_STORE_POINTERS, the created list stores pointers instead of
+ * copies of the added elements, and the compare function will be automatically set
+ * to cx_cmp_ptr().
+ *
+ * This macro behaves as if the list was created with cxKvListCreateSimple() and
+ * immediately followed up by cxKvListAsMap().
+ * If you want to use the returned object as a list, you can call cxKvListAsList() later.
+ *
+ * @param elem_size (@c size_t) the size of each element in bytes
+ * @return (@c CxMap*) the created list wrapped into the CxMap interface
+ * @see cxKvListAsMap()
+ * @see cxKvListAsList()
+ */
+#define cxKvListCreateAsMapSimple(elem_size) cxKvListCreateAsMap(NULL, NULL, elem_size)
+
+/**
+ * Converts a map pointer belonging to a key-value-List back to the original list pointer.
+ *
+ * @param map a map pointer that was returned by a call to cxKvListAsMap()
+ * @return the original list pointer
+ */
+cx_attr_nodiscard cx_attr_nonnull cx_attr_returns_nonnull
+CX_EXPORT CxList *cxKvListAsList(CxMap *map);
+
+/**
+ * Converts a map pointer belonging to a key-value-List back to the original list pointer.
+ *
+ * @param list a list created by cxKvListCreate() or cxKvListCreateSimple()
+ * @return a map pointer that lets you use the list as if it was a map
+ */
+cx_attr_nodiscard cx_attr_nonnull cx_attr_returns_nonnull
+CX_EXPORT CxMap *cxKvListAsMap(CxList *list);
+
+/**
+ * Sets or updates the key of a list item.
+ *
+ * This is, for example, useful when you have inserted an element using the CxList interface,
+ * and now you want to associate this element with a key.
+ *
+ * @param list the list
+ * @param index the index of the element in the list
+ * @param key the key
+ * @retval zero success
+ * @retval non-zero memory allocation failure or the index is out of bounds
+ * @see cxKvListSetKey()
+ */
+cx_attr_nonnull
+CX_EXPORT int cx_kv_list_set_key(CxList *list, size_t index, CxHashKey key);
+
+/**
+ * Inserts an item into the list at the specified index and associates it with the specified key.
+ *
+ * @param list the list
+ * @param index the index the inserted element shall have
+ * @param key the key
+ * @param value the value
+ * @retval zero success
+ * @retval non-zero memory allocation failure or the index is out of bounds
+ * @see cxKvListInsert()
+ */
+cx_attr_nonnull
+CX_EXPORT int cx_kv_list_insert(CxList *list, size_t index, CxHashKey key, void *value);
+
+/**
+ * Sets or updates the key of a list item.
+ *
+ * This is, for example, useful when you have inserted an element using the CxList interface,
+ * and now you want to associate this element with a key.
+ *
+ * @param list (@c CxList*) the list
+ * @param index (@c size_t) the index of the element in the list
+ * @param key (any supported key type) the key
+ * @retval zero success
+ * @retval non-zero memory allocation failure or the index is out of bounds
+ * @see CX_HASH_KEY()
+ */
+#define cxKvListSetKey(list, index, key) cx_kv_list_set_key(list, index, CX_HASH_KEY(key))
+
+/**
+ * Inserts an item into the list at the specified index and associates it with the specified key.
+ *
+ * @param list (@c CxList*) the list
+ * @param index (@c size_t) the index the inserted element shall have
+ * @param key (any supported key type) the key
+ * @param value (@c void*) the value
+ * @retval zero success
+ * @retval non-zero memory allocation failure or the index is out of bounds
+ * @see CX_HASH_KEY()
+ */
+#define cxKvListInsert(list, index, key, value) cx_kv_list_insert(list, index, CX_HASH_KEY(key), value)
+
+
+/**
+ * Removes the key of a list item.
+ *
+ * This can be useful if you want to explicitly remove an item from the lookup map.
+ *
+ * If no key is associated with the item, nothing happens, and this function returns zero.
+ *
+ * @param list the list
+ * @param index the index of the element in the list
+ * @retval zero success
+ * @retval non-zero the index is out of bounds
+ */
+cx_attr_nonnull
+CX_EXPORT int cxKvListRemoveKey(CxList *list, size_t index);
+
+/**
+ * Returns the key of a list item.
+ *
+ * @param list the list
+ * @param index the index of the element in the list
+ * @return a pointer to the key or @c NULL when the index is out of bounds or the item does not have a key
+ */
+cx_attr_nonnull
+CX_EXPORT const CxHashKey *cxKvListGetKey(CxList *list, size_t index);
+
+/**
+ * Adds an item into the list and associates it with the specified key.
+ *
+ * @param list (@c CxList*) the list
+ * @param key (@c CxHashKey, @c char*, @c cxstring, or @c cxmutstr) the key
+ * @param value (@c void*) the value
+ * @retval zero success
+ * @retval non-zero memory allocation failure
+ */
+#define cxKvListAdd(list, key, value) cxKvListInsert(list, (list)->collection.size, key, value)
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif // UCX_KV_LIST_H
--- a/ucx/cx/linked_list.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/cx/linked_list.h	Sat Nov 08 23:06:11 2025 +0100
@@ -44,11 +44,43 @@
 #endif
 
 /**
+ * Metadata for a linked list.
+ */
+typedef struct cx_linked_list_s {
+    /** Base members. */
+    struct cx_list_s base;
+    /**
+     * Location of the prev pointer (mandatory).
+     */
+    off_t loc_prev;
+    /**
+     * Location of the next pointer (mandatory).
+     */
+    off_t loc_next;
+    /**
+     * Location of the payload (mandatory).
+     */
+    off_t loc_data;
+    /**
+     * Additional bytes to allocate @em behind the payload (e.g. for metadata).
+     */
+    size_t extra_data_len;
+    /**
+     * Pointer to the first node.
+     */
+    void *begin;
+    /**
+     * Pointer to the last node.
+     */
+    void *end;
+} cx_linked_list;
+
+/**
  * Allocates a linked list for storing elements with @p elem_size bytes each.
  *
  * If @p elem_size is #CX_STORE_POINTERS, the created list stores pointers instead of
- * copies of the added elements and the compare function will be automatically set
- * to cx_cmp_ptr(), if none is given.
+ * copies of the added elements, and the compare function will be automatically set
+ * to cx_cmp_ptr() if none is given.
  *
  * @param allocator the allocator for allocating the list nodes
  * (if @c NULL, the cxDefaultAllocator will be used)
@@ -58,15 +90,9 @@
  * @param elem_size the size of each element in bytes
  * @return the created list
  */
-cx_attr_nodiscard
-cx_attr_malloc
-cx_attr_dealloc(cxListFree, 1)
-cx_attr_export
-CxList *cxLinkedListCreate(
-        const CxAllocator *allocator,
-        cx_compare_func comparator,
-        size_t elem_size
-);
+cx_attr_nodiscard cx_attr_malloc cx_attr_dealloc(cxListFree, 1)
+CX_EXPORT CxList *cxLinkedListCreate(const CxAllocator *allocator,
+        cx_compare_func comparator, size_t elem_size);
 
 /**
  * Allocates a linked list for storing elements with @p elem_size bytes each.
@@ -76,25 +102,25 @@
  * after list creation or use cxLinkedListCreate().
  *
  * If @p elem_size is #CX_STORE_POINTERS, the created list stores pointers instead of
- * copies of the added elements and the compare function will be automatically set
+ * copies of the added elements, and the compare function will be automatically set
  * to cx_cmp_ptr().
  *
  * @param elem_size (@c size_t) the size of each element in bytes
  * @return (@c CxList*) the created list
  */
 #define cxLinkedListCreateSimple(elem_size) \
-    cxLinkedListCreate(NULL, NULL, elem_size)
+        cxLinkedListCreate(NULL, NULL, elem_size)
 
 /**
  * Finds the node at a certain index.
  *
  * This function can be used to start at an arbitrary position within the list.
- * If the search index is large than the start index, @p loc_advance must denote
- * the location of some sort of @c next pointer (i.e. a pointer to the next node).
+ * If the search index is larger than the start index, @p loc_advance must denote
+ * the location of a @c next pointer (i.e., a pointer to the next node).
  * But it is also possible that the search index is smaller than the start index
- * (e.g. in cases where traversing a list backwards is faster) in which case
- * @p loc_advance must denote the location of some sort of @c prev pointer
- * (i.e. a pointer to the previous node).
+ * (e.g., in cases where traversing a list backwards is faster).
+ * In that case @p loc_advance must denote the location of a @c prev pointer
+ * (i.e., a pointer to the previous node).
  *
  * @param start a pointer to the start node
  * @param start_index the start index
@@ -102,15 +128,9 @@
  * @param index the search index
  * @return the node found at the specified index
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-void *cx_linked_list_at(
-        const void *start,
-        size_t start_index,
-        ptrdiff_t loc_advance,
-        size_t index
-);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT void *cx_linked_list_at(const void *start,size_t start_index,
+        ptrdiff_t loc_advance, size_t index);
 
 /**
  * Finds the node containing an element within a linked list.
@@ -122,18 +142,12 @@
  * @param elem a pointer to the element to find
  * @param found_index an optional pointer where the index of the found node
  * (given that @p start has index 0) is stored
- * @return the index of the element, if found - unspecified if not found
+ * @return a pointer to the found node or @c NULL if no matching node was found
  */
 cx_attr_nonnull_arg(1, 4, 5)
-cx_attr_export
-void *cx_linked_list_find(
-        const void *start,
-        ptrdiff_t loc_advance,
-        ptrdiff_t loc_data,
-        cx_compare_func cmp_func,
-        const void *elem,
-        size_t *found_index
-);
+CX_EXPORT void *cx_linked_list_find(const void *start, ptrdiff_t loc_advance,
+        ptrdiff_t loc_data, cx_compare_func cmp_func, const void *elem,
+        size_t *found_index);
 
 /**
  * Finds the first node in a linked list.
@@ -146,13 +160,8 @@
  * @param loc_prev the location of the @c prev pointer
  * @return a pointer to the first node
  */
-cx_attr_nonnull
-cx_attr_returns_nonnull
-cx_attr_export
-void *cx_linked_list_first(
-        const void *node,
-        ptrdiff_t loc_prev
-);
+cx_attr_nonnull cx_attr_returns_nonnull
+CX_EXPORT void *cx_linked_list_first(const void *node, ptrdiff_t loc_prev);
 
 /**
  * Finds the last node in a linked list.
@@ -165,13 +174,8 @@
  * @param loc_next the location of the @c next pointer
  * @return a pointer to the last node
  */
-cx_attr_nonnull
-cx_attr_returns_nonnull
-cx_attr_export
-void *cx_linked_list_last(
-        const void *node,
-        ptrdiff_t loc_next
-);
+cx_attr_nonnull cx_attr_returns_nonnull
+CX_EXPORT void *cx_linked_list_last(const void *node, ptrdiff_t loc_next);
 
 /**
  * Finds the predecessor of a node in case it is not linked.
@@ -184,16 +188,11 @@
  * @return the node or @c NULL if @p node has no predecessor
  */
 cx_attr_nonnull
-cx_attr_export
-void *cx_linked_list_prev(
-        const void *begin,
-        ptrdiff_t loc_next,
-        const void *node
-);
+CX_EXPORT void *cx_linked_list_prev(const void *begin, ptrdiff_t loc_next, const void *node);
 
 /**
  * Adds a new node to a linked list.
- * The node must not be part of any list already.
+ * The node must not be part of any list yet.
  *
  * @remark One of the pointers @p begin or @p end may be @c NULL, but not both.
  *
@@ -204,18 +203,11 @@
  * @param new_node a pointer to the node that shall be appended
  */
 cx_attr_nonnull_arg(5)
-cx_attr_export
-void cx_linked_list_add(
-        void **begin,
-        void **end,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next,
-        void *new_node
-);
+CX_EXPORT void cx_linked_list_add(void **begin, void **end, ptrdiff_t loc_prev, ptrdiff_t loc_next, void *new_node);
 
 /**
  * Prepends a new node to a linked list.
- * The node must not be part of any list already.
+ * The node must not be part of any list yet.
  *
  * @remark One of the pointers @p begin or @p end may be @c NULL, but not both.
  *
@@ -226,14 +218,7 @@
  * @param new_node a pointer to the node that shall be prepended
  */
 cx_attr_nonnull_arg(5)
-cx_attr_export
-void cx_linked_list_prepend(
-        void **begin,
-        void **end,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next,
-        void *new_node
-);
+CX_EXPORT void cx_linked_list_prepend(void **begin, void **end, ptrdiff_t loc_prev, ptrdiff_t loc_next, void *new_node);
 
 /**
  * Links two nodes.
@@ -244,13 +229,7 @@
  * @param loc_next the location of a @c next pointer within your node struct (required)
  */
 cx_attr_nonnull
-cx_attr_export
-void cx_linked_list_link(
-        void *left,
-        void *right,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next
-);
+CX_EXPORT void cx_linked_list_link(void *left, void *right, ptrdiff_t loc_prev, ptrdiff_t loc_next);
 
 /**
  * Unlinks two nodes.
@@ -263,17 +242,11 @@
  * @param loc_next the location of a @c next pointer within your node struct (required)
  */
 cx_attr_nonnull
-cx_attr_export
-void cx_linked_list_unlink(
-        void *left,
-        void *right,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next
-);
+CX_EXPORT void cx_linked_list_unlink(void *left, void *right, ptrdiff_t loc_prev, ptrdiff_t loc_next);
 
 /**
  * Inserts a new node after a given node of a linked list.
- * The new node must not be part of any list already.
+ * The new node must not be part of any list yet.
  *
  * @note If you specify @c NULL as the @p node to insert after, this function needs either the @p begin or
  * the @p end pointer to determine the start of the list. Then the new node will be prepended to the list.
@@ -286,19 +259,12 @@
  * @param new_node a pointer to the node that shall be inserted
  */
 cx_attr_nonnull_arg(6)
-cx_attr_export
-void cx_linked_list_insert(
-        void **begin,
-        void **end,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next,
-        void *node,
-        void *new_node
-);
+CX_EXPORT void cx_linked_list_insert(void **begin, void **end,
+        ptrdiff_t loc_prev, ptrdiff_t loc_next, void *node, void *new_node);
 
 /**
  * Inserts a chain of nodes after a given node of a linked list.
- * The chain must not be part of any list already.
+ * The chain must not be part of any list yet.
  *
  * If you do not explicitly specify the end of the chain, it will be determined by traversing
  * the @c next pointer.
@@ -317,20 +283,12 @@
  * @param insert_end a pointer to the last node of the chain (or NULL if the last node shall be determined)
  */
 cx_attr_nonnull_arg(6)
-cx_attr_export
-void cx_linked_list_insert_chain(
-        void **begin,
-        void **end,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next,
-        void *node,
-        void *insert_begin,
-        void *insert_end
-);
+CX_EXPORT void cx_linked_list_insert_chain(void **begin, void **end,
+        ptrdiff_t loc_prev, ptrdiff_t loc_next, void *node, void *insert_begin, void *insert_end);
 
 /**
  * Inserts a node into a sorted linked list.
- * The new node must not be part of any list already.
+ * The new node must not be part of any list yet.
  *
  * If the list starting with the node pointed to by @p begin is not sorted
  * already, the behavior is undefined.
@@ -343,26 +301,19 @@
  * @param cmp_func a compare function that will receive the node pointers
  */
 cx_attr_nonnull_arg(1, 5, 6)
-cx_attr_export
-void cx_linked_list_insert_sorted(
-        void **begin,
-        void **end,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next,
-        void *new_node,
-        cx_compare_func cmp_func
-);
+CX_EXPORT void cx_linked_list_insert_sorted(void **begin, void **end,
+        ptrdiff_t loc_prev, ptrdiff_t loc_next, void *new_node, cx_compare_func cmp_func);
 
 /**
  * Inserts a chain of nodes into a sorted linked list.
- * The chain must not be part of any list already.
+ * The chain must not be part of any list yet.
  *
  * If either the list starting with the node pointed to by @p begin or the list
  * starting with @p insert_begin is not sorted, the behavior is undefined.
  *
  * @attention In contrast to cx_linked_list_insert_chain(), the source chain
  * will be broken and inserted into the target list so that the resulting list
- * will be sorted according to @p cmp_func. That means, each node in the source
+ * will be sorted according to @p cmp_func. That means each node in the source
  * chain may be re-linked with nodes from the target list.
  *
  * @param begin a pointer to the beginning node pointer (required)
@@ -373,20 +324,55 @@
  * @param cmp_func a compare function that will receive the node pointers
  */
 cx_attr_nonnull_arg(1, 5, 6)
-cx_attr_export
-void cx_linked_list_insert_sorted_chain(
-        void **begin,
-        void **end,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next,
-        void *insert_begin,
-        cx_compare_func cmp_func
-);
+CX_EXPORT void cx_linked_list_insert_sorted_chain(void **begin, void **end,
+        ptrdiff_t loc_prev, ptrdiff_t loc_next, void *insert_begin, cx_compare_func cmp_func);
+
+/**
+ * Inserts a node into a sorted linked list if no other node with the same value already exists.
+ * The new node must not be part of any list yet.
+ *
+ * If the list starting with the node pointed to by @p begin is not sorted
+ * already, the behavior is undefined.
+ *
+ * @param begin a pointer to the beginning node pointer (required)
+ * @param end a pointer to the end node pointer (if your list has one)
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if your struct does not have one)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
+ * @param new_node a pointer to the node that shall be inserted
+ * @param cmp_func a compare function that will receive the node pointers
+ * @retval zero when the node was inserted
+ * @retval non-zero when a node with the same value already exists
+ */
+cx_attr_nonnull_arg(1, 5, 6)
+CX_EXPORT int cx_linked_list_insert_unique(void **begin, void **end,
+        ptrdiff_t loc_prev, ptrdiff_t loc_next, void *new_node, cx_compare_func cmp_func);
+
+/**
+ * Inserts a chain of nodes into a sorted linked list, avoiding duplicates.
+ * The chain must not be part of any list yet.
+ *
+ * If either the list starting with the node pointed to by @p begin or the list
+ * starting with @p insert_begin is not sorted, the behavior is undefined.
+ *
+ * @attention In contrast to cx_linked_list_insert_sorted(), not all nodes of the
+ * chain might be added. This function returns a new chain consisting of all the duplicates.
+ *
+ * @param begin a pointer to the beginning node pointer (required)
+ * @param end a pointer to the end node pointer (if your list has one)
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if your struct does not have one)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
+ * @param insert_begin a pointer to the first node of the chain that shall be inserted
+ * @param cmp_func a compare function that will receive the node pointers
+ * @return a pointer to a new chain with all duplicates that were not inserted (or @c NULL when there were no duplicates)
+ */
+cx_attr_nonnull_arg(1, 5, 6) cx_attr_nodiscard
+CX_EXPORT void *cx_linked_list_insert_unique_chain(void **begin, void **end,
+        ptrdiff_t loc_prev, ptrdiff_t loc_next, void *insert_begin, cx_compare_func cmp_func);
 
 /**
  * Removes a chain of nodes from the linked list.
  *
- * If one of the nodes to remove is the beginning (resp. end) node of the list and if @p begin (resp. @p end)
+ * If one of the nodes to remove is the beginning (resp. end) node of the list, and if @p begin (resp. @p end)
  * addresses are provided, the pointers are adjusted accordingly.
  *
  * The following combinations of arguments are valid (more arguments are optional):
@@ -405,20 +391,13 @@
  * @return the actual number of nodes that were removed (can be less when the list did not have enough nodes)
  */
 cx_attr_nonnull_arg(5)
-cx_attr_export
-size_t cx_linked_list_remove_chain(
-        void **begin,
-        void **end,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next,
-        void *node,
-        size_t num
-);
+CX_EXPORT size_t cx_linked_list_remove_chain(void **begin, void **end,
+        ptrdiff_t loc_prev, ptrdiff_t loc_next, void *node, size_t num);
 
 /**
  * Removes a node from the linked list.
  *
- * If the node to remove is the beginning (resp. end) node of the list and if @p begin (resp. @p end)
+ * If the node to remove is the beginning (resp. end) node of the list, and if @p begin (resp. @p end)
  * addresses are provided, the pointers are adjusted accordingly.
  *
  * The following combinations of arguments are valid (more arguments are optional):
@@ -435,15 +414,8 @@
  * @param node the node to remove
  */
 cx_attr_nonnull_arg(5)
-static inline void cx_linked_list_remove(
-        void **begin,
-        void **end,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next,
-        void *node
-) {
-    cx_linked_list_remove_chain(begin, end, loc_prev, loc_next, node, 1);
-}
+CX_EXPORT void cx_linked_list_remove(void **begin, void **end,
+        ptrdiff_t loc_prev, ptrdiff_t loc_next, void *node);
 
 /**
  * Determines the size of a linked list starting with @p node.
@@ -453,11 +425,7 @@
  * @return the size of the list or zero if @p node is @c NULL
  */
 cx_attr_nodiscard
-cx_attr_export
-size_t cx_linked_list_size(
-        const void *node,
-        ptrdiff_t loc_next
-);
+CX_EXPORT size_t cx_linked_list_size(const void *node, ptrdiff_t loc_next);
 
 /**
  * Sorts a linked list based on a comparison function.
@@ -482,21 +450,14 @@
  * @param cmp_func the compare function defining the sort order
  */
 cx_attr_nonnull_arg(1, 6)
-cx_attr_export
-void cx_linked_list_sort(
-        void **begin,
-        void **end,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next,
-        ptrdiff_t loc_data,
-        cx_compare_func cmp_func
-);
+CX_EXPORT void cx_linked_list_sort(void **begin, void **end,
+        ptrdiff_t loc_prev, ptrdiff_t loc_next, ptrdiff_t loc_data, cx_compare_func cmp_func);
 
 
 /**
  * Compares two lists element wise.
  *
- * @attention Both list must have the same structure.
+ * @attention Both lists must have the same structure.
  *
  * @param begin_left the beginning of the left list (@c NULL denotes an empty list)
  * @param begin_right the beginning of the right list  (@c NULL denotes an empty list)
@@ -507,14 +468,8 @@
  * right list, positive if the left list is larger than the right list, zero if both lists are equal.
  */
 cx_attr_nonnull_arg(5)
-cx_attr_export
-int cx_linked_list_compare(
-        const void *begin_left,
-        const void *begin_right,
-        ptrdiff_t loc_advance,
-        ptrdiff_t loc_data,
-        cx_compare_func cmp_func
-);
+CX_EXPORT int cx_linked_list_compare(const void *begin_left, const void *begin_right,
+        ptrdiff_t loc_advance, ptrdiff_t loc_data, cx_compare_func cmp_func);
 
 /**
  * Reverses the order of the nodes in a linked list.
@@ -525,13 +480,7 @@
  * @param loc_next the location of a @c next pointer within your node struct (required)
  */
 cx_attr_nonnull_arg(1)
-cx_attr_export
-void cx_linked_list_reverse(
-        void **begin,
-        void **end,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next
-);
+CX_EXPORT void cx_linked_list_reverse(void **begin, void **end, ptrdiff_t loc_prev, ptrdiff_t loc_next);
 
 #ifdef __cplusplus
 } // extern "C"
--- a/ucx/cx/list.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/cx/list.h	Sat Nov 08 23:06:11 2025 +0100
@@ -80,46 +80,41 @@
 
     /**
      * Member function for inserting a single element.
-     * The data pointer may be @c NULL in which case the function shall only allocate memory.
+     * The data pointer may be @c NULL, in which case the function shall only allocate memory.
      * Returns a pointer to the allocated memory or @c NULL if allocation fails.
      */
-    void *(*insert_element)(
-            struct cx_list_s *list,
-            size_t index,
-            const void *data
-    );
+    void *(*insert_element)(struct cx_list_s *list, size_t index, const void *data);
 
     /**
      * Member function for inserting multiple elements.
      *
+     * The data pointer may be @c NULL, in which case the function shall only allocate memory.
+     * Returns the number of successfully inserted or allocated elements.
+     *
      * @see cx_list_default_insert_array()
      */
-    size_t (*insert_array)(
-            struct cx_list_s *list,
-            size_t index,
-            const void *data,
-            size_t n
-    );
+    size_t (*insert_array)(struct cx_list_s *list, size_t index, const void *data, size_t n);
 
     /**
      * Member function for inserting sorted elements into a sorted list.
+     * Returns the number of successfully inserted elements.
      *
      * @see cx_list_default_insert_sorted()
      */
-    size_t (*insert_sorted)(
-            struct cx_list_s *list,
-            const void *sorted_data,
-            size_t n
-    );
+    size_t (*insert_sorted)(struct cx_list_s *list, const void *sorted_data, size_t n);
+
+    /**
+     * Member function for inserting multiple elements if they do not exist.
+     * Implementations shall return the number of successfully processed elements
+     * (including those which were not added because they are already contained).
+     * @see cx_list_default_insert_unique()
+     */
+    size_t (*insert_unique)(struct cx_list_s *list, const void *sorted_data, size_t n);
 
     /**
      * Member function for inserting an element relative to an iterator position.
      */
-    int (*insert_iter)(
-            struct cx_iterator_s *iter,
-            const void *elem,
-            int prepend
-    );
+    int (*insert_iter)(struct cx_iterator_s *iter, const void *elem, int prepend);
 
     /**
      * Member function for removing elements.
@@ -131,12 +126,7 @@
      * The function SHALL return the actual number of elements removed, which
      * might be lower than @p num when going out of bounds.
      */
-    size_t (*remove)(
-            struct cx_list_s *list,
-            size_t index,
-            size_t num,
-            void *targetbuf
-    );
+    size_t (*remove)(struct cx_list_s *list, size_t index, size_t num, void *targetbuf);
 
     /**
      * Member function for removing all elements.
@@ -148,28 +138,17 @@
      *
      * @see cx_list_default_swap()
      */
-    int (*swap)(
-            struct cx_list_s *list,
-            size_t i,
-            size_t j
-    );
+    int (*swap)(struct cx_list_s *list, size_t i, size_t j);
 
     /**
      * Member function for element lookup.
      */
-    void *(*at)(
-            const struct cx_list_s *list,
-            size_t index
-    );
+    void *(*at)(const struct cx_list_s *list, size_t index);
 
     /**
      * Member function for finding and optionally removing an element.
      */
-    size_t (*find_remove)(
-            struct cx_list_s *list,
-            const void *elem,
-            bool remove
-    );
+    size_t (*find_remove)(struct cx_list_s *list, const void *elem, bool remove);
 
     /**
      * Member function for sorting the list.
@@ -181,12 +160,9 @@
     /**
      * Optional member function for comparing this list
      * to another list of the same type.
-     * If set to @c NULL, comparison won't be optimized.
+     * If set to @c NULL, the comparison won't be optimized.
      */
-    int (*compare)(
-            const struct cx_list_s *list,
-            const struct cx_list_s *other
-    );
+    int (*compare)(const struct cx_list_s *list, const struct cx_list_s *other);
 
     /**
      * Member function for reversing the order of the items.
@@ -196,11 +172,7 @@
     /**
      * Member function for returning an iterator pointing to the specified index.
      */
-    struct cx_iterator_s (*iterator)(
-            const struct cx_list_s *list,
-            size_t index,
-            bool backward
-    );
+    struct cx_iterator_s (*iterator)(const struct cx_list_s *list, size_t index, bool backward);
 };
 
 /**
@@ -213,11 +185,10 @@
  *
  * Writing to that list is not allowed.
  *
- * You can use this is a placeholder for initializing CxList pointers
+ * You can use this as a placeholder for initializing CxList pointers
  * for which you do not want to reserve memory right from the beginning.
  */
-cx_attr_export
-extern CxList *const cxEmptyList;
+CX_EXPORT extern CxList *const cxEmptyList;
 
 /**
  * Default implementation of an array insert.
@@ -234,13 +205,8 @@
  * @return the number of elements actually inserted
  */
 cx_attr_nonnull
-cx_attr_export
-size_t cx_list_default_insert_array(
-        struct cx_list_s *list,
-        size_t index,
-        const void *data,
-        size_t n
-);
+CX_EXPORT size_t cx_list_default_insert_array(struct cx_list_s *list,
+        size_t index, const void *data, size_t n);
 
 /**
  * Default implementation of a sorted insert.
@@ -259,12 +225,28 @@
  * @return the number of elements actually inserted
  */
 cx_attr_nonnull
-cx_attr_export
-size_t cx_list_default_insert_sorted(
-        struct cx_list_s *list,
-        const void *sorted_data,
-        size_t n
-);
+CX_EXPORT size_t cx_list_default_insert_sorted(struct cx_list_s *list,
+        const void *sorted_data, size_t n);
+
+/**
+ * Default implementation of an array insert where only elements are inserted when they don't exist in the list.
+ *
+ * This function is similar to cx_list_default_insert_sorted(), except it skips elements that are already in the list.
+ *
+ * @note The return value of this function denotes the number of elements from the @p sorted_data that are definitely
+ * contained in the list after completing the call. It is @em not the number of elements that were newly inserted.
+ * That means, when no error occurred, the return value should be @p n.
+ *
+ * Use this in your own list class if you do not want to implement an optimized version for your list.
+ *
+ * @param list the list
+ * @param sorted_data a pointer to the array of pre-sorted data to insert
+ * @param n the number of elements to insert
+ * @return the number of elements from the @p sorted_data that are definitely present in the list after this call
+ */
+cx_attr_nonnull
+CX_EXPORT size_t cx_list_default_insert_unique(struct cx_list_s *list,
+        const void *sorted_data, size_t n);
 
 /**
  * Default unoptimized sort implementation.
@@ -278,8 +260,7 @@
  * @param list the list that shall be sorted
  */
 cx_attr_nonnull
-cx_attr_export
-void cx_list_default_sort(struct cx_list_s *list);
+CX_EXPORT void cx_list_default_sort(struct cx_list_s *list);
 
 /**
  * Default unoptimized swap implementation.
@@ -295,20 +276,19 @@
  * allocation for the temporary buffer fails
  */
 cx_attr_nonnull
-cx_attr_export
-int cx_list_default_swap(struct cx_list_s *list, size_t i, size_t j);
+CX_EXPORT int cx_list_default_swap(struct cx_list_s *list, size_t i, size_t j);
 
 /**
  * Initializes a list struct.
  *
  * Only use this function if you are creating your own list implementation.
  * The purpose of this function is to be called in the initialization code
- * of your list, to set certain members correctly.
+ * of your list to set certain members correctly.
  *
  * This is particularly important when you want your list to support
  * #CX_STORE_POINTERS as @p elem_size. This function will wrap the list
  * class accordingly and make sure that you can implement your list as if
- * it was only storing objects and the wrapper will automatically enable
+ * it was only storing objects, and the wrapper will automatically enable
  * the feature of storing pointers.
  *
  * @par Example
@@ -343,14 +323,9 @@
  * @param elem_size the size of one element
  */
 cx_attr_nonnull_arg(1, 2, 3)
-cx_attr_export
-void cx_list_init(
-    struct cx_list_s *list,
-    struct cx_list_class_s *cl,
-    const struct cx_allocator_s *allocator,
-    cx_compare_func comparator,
-    size_t elem_size
-);
+CX_EXPORT void cx_list_init(struct cx_list_s *list,
+    struct cx_list_class_s *cl, const struct cx_allocator_s *allocator,
+    cx_compare_func comparator, size_t elem_size);
 
 /**
  * Returns the number of elements currently stored in the list.
@@ -359,9 +334,7 @@
  * @return the number of currently stored elements
  */
 cx_attr_nonnull
-static inline size_t cxListSize(const CxList *list) {
-    return list->collection.size;
-}
+CX_EXPORT size_t cxListSize(const CxList *list);
 
 /**
  * Adds an item to the end of the list.
@@ -374,13 +347,7 @@
  * @see cxListEmplace()
  */
 cx_attr_nonnull
-static inline int cxListAdd(
-        CxList *list,
-        const void *elem
-) {
-    list->collection.sorted = false;
-    return list->cl->insert_element(list, list->collection.size, elem) == NULL;
-}
+CX_EXPORT int cxListAdd(CxList *list, const void *elem);
 
 /**
  * Adds multiple items to the end of the list.
@@ -390,28 +357,22 @@
  * If there is not enough memory to add all elements, the returned value is
  * less than @p n.
  *
- * If this list is storing pointers instead of objects @p array is expected to
+ * If this list is storing pointers instead of objects, @p array is expected to
  * be an array of pointers.
  *
  * @param list the list
  * @param array a pointer to the elements to add
  * @param n the number of elements to add
  * @return the number of added elements
+ * @see cxListEmplaceArray()
  */
 cx_attr_nonnull
-static inline size_t cxListAddArray(
-        CxList *list,
-        const void *array,
-        size_t n
-) {
-    list->collection.sorted = false;
-    return list->cl->insert_array(list, list->collection.size, array, n);
-}
+CX_EXPORT size_t cxListAddArray(CxList *list, const void *array, size_t n);
 
 /**
  * Inserts an item at the specified index.
  *
- * If @p index equals the list @c size, this is effectively cxListAdd().
+ * If the @p index equals the list @c size, this is effectively cxListAdd().
  *
  * @param list the list
  * @param index the index the element shall have
@@ -423,14 +384,7 @@
  * @see cxListEmplaceAt()
  */
 cx_attr_nonnull
-static inline int cxListInsert(
-        CxList *list,
-        size_t index,
-        const void *elem
-) {
-    list->collection.sorted = false;
-    return list->cl->insert_element(list, index, elem) == NULL;
-}
+CX_EXPORT int cxListInsert(CxList *list, size_t index, const void *elem);
 
 /**
  * Allocates memory for an element at the specified index and returns a pointer to that memory.
@@ -441,14 +395,11 @@
  * @param index the index where to emplace the element
  * @return a pointer to the allocated memory; @c NULL when the operation fails, or the index is out-of-bounds
  * @see cxListEmplace()
+ * @see cxListEmplaceArrayAt()
  * @see cxListInsert()
  */
 cx_attr_nonnull
-static inline void *cxListEmplaceAt(CxList *list, size_t index) {
-    list->collection.sorted = false;
-    return list->cl->insert_element(list, index, NULL);
-}
-
+CX_EXPORT void *cxListEmplaceAt(CxList *list, size_t index);
 
 /**
  * Allocates memory for an element at the end of the list and returns a pointer to that memory.
@@ -461,10 +412,46 @@
  * @see cxListAdd()
  */
 cx_attr_nonnull
-static inline void *cxListEmplace(CxList *list) {
-    list->collection.sorted = false;
-    return list->cl->insert_element(list, list->collection.size, NULL);
-}
+CX_EXPORT void *cxListEmplace(CxList *list);
+
+/**
+ * Allocates memory for multiple elements and returns an iterator.
+ *
+ * The iterator will only iterate over the successfully allocated elements.
+ * The @c elem_count attribute is set to that number, and the @c index attribute
+ * will range from zero to @c elem_count minus one.
+ *
+ * @remark When the list is storing pointers, the iterator will iterate over
+ * the @c void** elements.
+ *
+ * @param list the list
+ * @param index the index where to insert the new data
+ * @param n the number of elements for which to allocate the memory
+ * @return an iterator, iterating over the new memory
+ * @see cxListEmplaceAt()
+ * @see cxListInsertArray()
+ */
+cx_attr_nonnull
+CX_EXPORT CxIterator cxListEmplaceArrayAt(CxList *list, size_t index, size_t n);
+
+/**
+ * Allocates memory for multiple elements and returns an iterator.
+ *
+ * The iterator will only iterate over the successfully allocated elements.
+ * The @c elem_count attribute is set to that number, and the @c index attribute
+ * will range from zero to @c elem_count minus one.
+ *
+ * @remark When the list is storing pointers, the iterator will iterate over
+ * the @c void** elements.
+ *
+ * @param list the list
+ * @param n the number of elements for which to allocate the memory
+ * @return an iterator, iterating over the new memory
+ * @see cxListEmplace()
+ * @see cxListAddArray()
+ */
+cx_attr_nonnull
+CX_EXPORT CxIterator cxListEmplaceArray(CxList *list, size_t n);
 
 /**
  * Inserts an item into a sorted list.
@@ -477,18 +464,27 @@
  * @retval non-zero memory allocation failure
  */
 cx_attr_nonnull
-static inline int cxListInsertSorted(
-        CxList *list,
-        const void *elem
-) {
-    list->collection.sorted = true; // guaranteed by definition
-    const void *data = list->collection.store_pointer ? &elem : elem;
-    return list->cl->insert_sorted(list, data, 1) == 0;
-}
+CX_EXPORT int cxListInsertSorted(CxList *list, const void *elem);
+
+/**
+ * Inserts an item into a list if it does not exist.
+ *
+ * If the list is not sorted already, this function will check all elements
+ * and append the new element when it was not found.
+ * It is strongly recommended to use this function only on sorted lists, where
+ * the element, if it is not contained, is inserted at the correct position.
+ *
+ * @param list the list
+ * @param elem a pointer to the element to add
+ * @retval zero success (also when the element was already in the list)
+ * @retval non-zero memory allocation failure
+ */
+cx_attr_nonnull
+CX_EXPORT int cxListInsertUnique(CxList *list, const void *elem);
 
 /**
  * Inserts multiple items to the list at the specified index.
- * If @p index equals the list size, this is effectively cxListAddArray().
+ * If the @p index equals the list size, this is effectively cxListAddArray().
  *
  * This method is usually more efficient than invoking cxListInsert()
  * multiple times.
@@ -496,7 +492,7 @@
  * If there is not enough memory to add all elements, the returned value is
  * less than @p n.
  *
- * If this list is storing pointers instead of objects @p array is expected to
+ * If this list is storing pointers instead of objects, @p array is expected to
  * be an array of pointers.
  *
  * @param list the list
@@ -504,28 +500,21 @@
  * @param array a pointer to the elements to add
  * @param n the number of elements to add
  * @return the number of added elements
+ * @see cxListEmplaceArrayAt()
  */
 cx_attr_nonnull
-static inline size_t cxListInsertArray(
-        CxList *list,
-        size_t index,
-        const void *array,
-        size_t n
-) {
-    list->collection.sorted = false;
-    return list->cl->insert_array(list, index, array, n);
-}
+CX_EXPORT size_t cxListInsertArray(CxList *list, size_t index, const void *array, size_t n);
 
 /**
  * Inserts a sorted array into a sorted list.
  *
- * This method is usually more efficient than inserting each element separately,
+ * This method is usually more efficient than inserting each element separately
  * because consecutive chunks of sorted data are inserted in one pass.
  *
  * If there is not enough memory to add all elements, the returned value is
  * less than @p n.
  *
- * If this list is storing pointers instead of objects @p array is expected to
+ * If this list is storing pointers instead of objects, @p array is expected to
  * be an array of pointers.
  *
  * If the list is not sorted already, the behavior is undefined.
@@ -536,14 +525,42 @@
  * @return the number of added elements
  */
 cx_attr_nonnull
-static inline size_t cxListInsertSortedArray(
-        CxList *list,
-        const void *array,
-        size_t n
-) {
-    list->collection.sorted = true; // guaranteed by definition
-    return list->cl->insert_sorted(list, array, n);
-}
+CX_EXPORT size_t cxListInsertSortedArray(CxList *list, const void *array, size_t n);
+
+/**
+ * Inserts an array into a list, skipping duplicates.
+ *
+ * The @p list does not need to be sorted (in contrast to cxListInsertSortedArray()).
+ * But it is strongly recommended to use this function only on sorted lists,
+ * because otherwise it will fall back to an inefficient algorithm which inserts
+ * all elements one by one.
+ * If the @p list is not sorted, the @p array also does not need to be sorted.
+ * But when the @p list is sorted, the @p array must also be sorted.
+ *
+ * This method is usually more efficient than inserting each element separately
+ * because consecutive chunks of sorted data are inserted in one pass.
+ *
+ * If there is not enough memory to add all elements, the returned value is
+ * less than @p n.
+ *
+ * @note The return value of this function denotes the number of elements
+ * from the @p sorted_data that are definitely contained in the list after
+ * completing the call. It is @em not the number of elements that were newly
+ * inserted. That means, when no error occurred, the return value should
+ * be @p n.
+ *
+ * If this list is storing pointers instead of objects @p array is expected to
+ * be an array of pointers.
+ *
+ * @param list the list
+ * @param array a pointer to the elements to add
+ * @param n the number of elements to add
+ * @return the number of added elements
+ *
+ * @return the number of elements from the @p sorted_data that are definitely present in the list after this call
+ */
+cx_attr_nonnull
+CX_EXPORT size_t cxListInsertUniqueArray(CxList *list, const void *array, size_t n);
 
 /**
  * Inserts an element after the current location of the specified iterator.
@@ -562,14 +579,7 @@
  * @see cxListInsertBefore()
  */
 cx_attr_nonnull
-static inline int cxListInsertAfter(
-        CxIterator *iter,
-        const void *elem
-) {
-    CxList* list = (CxList*)iter->src_handle.m;
-    list->collection.sorted = false;
-    return list->cl->insert_iter(iter, elem, 0);
-}
+CX_EXPORT int cxListInsertAfter(CxIterator *iter, const void *elem);
 
 /**
  * Inserts an element before the current location of the specified iterator.
@@ -588,14 +598,7 @@
  * @see cxListInsertAfter()
  */
 cx_attr_nonnull
-static inline int cxListInsertBefore(
-        CxIterator *iter,
-        const void *elem
-) {
-    CxList* list = (CxList*)iter->src_handle.m;
-    list->collection.sorted = false;
-    return list->cl->insert_iter(iter, elem, 1);
-}
+CX_EXPORT int cxListInsertBefore(CxIterator *iter, const void *elem);
 
 /**
  * Removes the element at the specified index.
@@ -609,12 +612,7 @@
  * @retval non-zero index out of bounds
  */
 cx_attr_nonnull
-static inline int cxListRemove(
-        CxList *list,
-        size_t index
-) {
-    return list->cl->remove(list, index, 1, NULL) == 0;
-}
+CX_EXPORT int cxListRemove(CxList *list, size_t index);
 
 /**
  * Removes and returns the element at the specified index.
@@ -629,15 +627,8 @@
  * @retval zero success
  * @retval non-zero index out of bounds
  */
-cx_attr_nonnull
-cx_attr_access_w(3)
-static inline int cxListRemoveAndGet(
-        CxList *list,
-        size_t index,
-        void *targetbuf
-) {
-    return list->cl->remove(list, index, 1, targetbuf) == 0;
-}
+cx_attr_nonnull cx_attr_access_w(3)
+CX_EXPORT int cxListRemoveAndGet(CxList *list, size_t index, void *targetbuf);
 
 /**
  * Removes and returns the first element of the list.
@@ -649,18 +640,12 @@
  * @param list the list
  * @param targetbuf a buffer where to copy the element
  * @retval zero success
- * @retval non-zero list is empty
+ * @retval non-zero the list is empty
  * @see cxListPopFront()
  * @see cxListRemoveAndGetLast()
  */
-cx_attr_nonnull
-cx_attr_access_w(2)
-static inline int cxListRemoveAndGetFirst(
-    CxList *list,
-    void *targetbuf
-) {
-    return list->cl->remove(list, 0, 1, targetbuf) == 0;
-}
+cx_attr_nonnull cx_attr_access_w(2)
+CX_EXPORT int cxListRemoveAndGetFirst(CxList *list, void *targetbuf);
 
 /**
  * Removes and returns the first element of the list.
@@ -674,7 +659,7 @@
  * @param list (@c CxList*) the list
  * @param targetbuf (@c void*) a buffer where to copy the element
  * @retval zero success
- * @retval non-zero list is empty
+ * @retval non-zero the list is empty
  * @see cxListRemoveAndGetFirst()
  * @see cxListPop()
  */
@@ -691,17 +676,10 @@
  * @param list the list
  * @param targetbuf a buffer where to copy the element
  * @retval zero success
- * @retval non-zero list is empty
+ * @retval non-zero the list is empty
  */
-cx_attr_nonnull
-cx_attr_access_w(2)
-static inline int cxListRemoveAndGetLast(
-    CxList *list,
-    void *targetbuf
-) {
-    // note: index may wrap - member function will catch that
-    return list->cl->remove(list, list->collection.size - 1, 1, targetbuf) == 0;
-}
+cx_attr_nonnull cx_attr_access_w(2)
+CX_EXPORT int cxListRemoveAndGetLast(CxList *list, void *targetbuf);
 
 /**
  * Removes and returns the last element of the list.
@@ -715,7 +693,7 @@
  * @param list (@c CxList*) the list
  * @param targetbuf (@c void*) a buffer where to copy the element
  * @retval zero success
- * @retval non-zero list is empty
+ * @retval non-zero the list is empty
  * @see cxListRemoveAndGetLast()
  * @see cxListPopFront()
  */
@@ -737,13 +715,7 @@
  * @return the actual number of removed elements
  */
 cx_attr_nonnull
-static inline size_t cxListRemoveArray(
-        CxList *list,
-        size_t index,
-        size_t num
-) {
-    return list->cl->remove(list, index, num, NULL);
-}
+CX_EXPORT size_t cxListRemoveArray(CxList *list, size_t index, size_t num);
 
 /**
  * Removes and returns multiple elements starting at the specified index.
@@ -758,16 +730,8 @@
  * @param targetbuf a buffer where to copy the elements
  * @return the actual number of removed elements
  */
-cx_attr_nonnull
-cx_attr_access_w(4)
-static inline size_t cxListRemoveArrayAndGet(
-        CxList *list,
-        size_t index,
-        size_t num,
-        void *targetbuf
-) {
-    return list->cl->remove(list, index, num, targetbuf);
-}
+cx_attr_nonnull cx_attr_access_w(4)
+CX_EXPORT size_t cxListRemoveArrayAndGet(CxList *list, size_t index, size_t num, void *targetbuf);
 
 /**
  * Removes all elements from this list.
@@ -778,10 +742,7 @@
  * @param list the list
  */
 cx_attr_nonnull
-static inline void cxListClear(CxList *list) {
-    list->collection.sorted = true; // empty lists are always sorted
-    list->cl->clear(list);
-}
+CX_EXPORT void cxListClear(CxList *list);
 
 /**
  * Swaps two items in the list.
@@ -797,14 +758,7 @@
  * or the swap needed extra memory, but allocation failed
  */
 cx_attr_nonnull
-static inline int cxListSwap(
-        CxList *list,
-        size_t i,
-        size_t j
-) {
-    list->collection.sorted = false;
-    return list->cl->swap(list, i, j);
-}
+CX_EXPORT int cxListSwap(CxList *list, size_t i, size_t j);
 
 /**
  * Returns a pointer to the element at the specified index.
@@ -816,12 +770,7 @@
  * @return a pointer to the element or @c NULL if the index is out of bounds
  */
 cx_attr_nonnull
-static inline void *cxListAt(
-        const CxList *list,
-        size_t index
-) {
-    return list->cl->at(list, index);
-}
+CX_EXPORT void *cxListAt(const CxList *list, size_t index);
 
 /**
  * Returns a pointer to the first element.
@@ -832,9 +781,7 @@
  * @return a pointer to the first element or @c NULL if the list is empty
  */
 cx_attr_nonnull
-static inline void *cxListFirst(const CxList *list) {
-    return list->cl->at(list, 0);
-}
+CX_EXPORT void *cxListFirst(const CxList *list);
 
 /**
  * Returns a pointer to the last element.
@@ -845,12 +792,13 @@
  * @return a pointer to the last element or @c NULL if the list is empty
  */
 cx_attr_nonnull
-static inline void *cxListLast(const CxList *list) {
-    return list->cl->at(list, list->collection.size - 1);
-}
+CX_EXPORT void *cxListLast(const CxList *list);
 
 /**
- * Sets the element at the specified index in the list
+ * Sets the element at the specified index in the list.
+ *
+ * This overwrites the element in-place without calling any destructor
+ * on the overwritten element.
  *
  * @param list the list to set the element in
  * @param index the index to set the element at
@@ -859,12 +807,7 @@
  * @retval non-zero when index is out of bounds
  */
 cx_attr_nonnull
-cx_attr_export
-int cxListSet(
-        CxList *list,
-        size_t index,
-        const void *elem
-);
+CX_EXPORT int cxListSet(CxList *list, size_t index, const void *elem);
 
 /**
  * Returns an iterator pointing to the item at the specified index.
@@ -878,13 +821,7 @@
  * @return a new iterator
  */
 cx_attr_nodiscard
-static inline CxIterator cxListIteratorAt(
-        const CxList *list,
-        size_t index
-) {
-    if (list == NULL) list = cxEmptyList;
-    return list->cl->iterator(list, index, false);
-}
+CX_EXPORT CxIterator cxListIteratorAt(const CxList *list, size_t index);
 
 /**
  * Returns a backwards iterator pointing to the item at the specified index.
@@ -898,50 +835,7 @@
  * @return a new iterator
  */
 cx_attr_nodiscard
-static inline CxIterator cxListBackwardsIteratorAt(
-        const CxList *list,
-        size_t index
-) {
-    if (list == NULL) list = cxEmptyList;
-    return list->cl->iterator(list, index, true);
-}
-
-/**
- * Returns a mutating iterator pointing to the item at the specified index.
- *
- * The returned iterator is position-aware.
- *
- * If the index is out of range or @p list is @c NULL, a past-the-end iterator will be returned.
- *
- * @param list the list
- * @param index the index where the iterator shall point at
- * @return a new iterator
- */
-cx_attr_nodiscard
-cx_attr_export
-CxIterator cxListMutIteratorAt(
-        CxList *list,
-        size_t index
-);
-
-/**
- * Returns a mutating backwards iterator pointing to the item at the
- * specified index.
- *
- * The returned iterator is position-aware.
- *
- * If the index is out of range or @p list is @c NULL, a past-the-end iterator will be returned.
- *
- * @param list the list
- * @param index the index where the iterator shall point at
- * @return a new iterator
- */
-cx_attr_nodiscard
-cx_attr_export
-CxIterator cxListMutBackwardsIteratorAt(
-        CxList *list,
-        size_t index
-);
+CX_EXPORT CxIterator cxListBackwardsIteratorAt(const CxList *list, size_t index);
 
 /**
  * Returns an iterator pointing to the first item of the list.
@@ -954,27 +848,7 @@
  * @return a new iterator
  */
 cx_attr_nodiscard
-static inline CxIterator cxListIterator(const CxList *list) {
-    if (list == NULL) list = cxEmptyList;
-    return list->cl->iterator(list, 0, false);
-}
-
-/**
- * Returns a mutating iterator pointing to the first item of the list.
- *
- * The returned iterator is position-aware.
- *
- * If the list is empty or @c NULL, a past-the-end iterator will be returned.
- *
- * @param list the list
- * @return a new iterator
- */
-cx_attr_nodiscard
-static inline CxIterator cxListMutIterator(CxList *list) {
-    if (list == NULL) list = cxEmptyList;
-    return cxListMutIteratorAt(list, 0);
-}
-
+CX_EXPORT CxIterator cxListIterator(const CxList *list);
 
 /**
  * Returns a backwards iterator pointing to the last item of the list.
@@ -987,26 +861,7 @@
  * @return a new iterator
  */
 cx_attr_nodiscard
-static inline CxIterator cxListBackwardsIterator(const CxList *list) {
-    if (list == NULL) list = cxEmptyList;
-    return list->cl->iterator(list, list->collection.size - 1, true);
-}
-
-/**
- * Returns a mutating backwards iterator pointing to the last item of the list.
- *
- * The returned iterator is position-aware.
- *
- * If the list is empty or @c NULL, a past-the-end iterator will be returned.
- *
- * @param list the list
- * @return a new iterator
- */
-cx_attr_nodiscard
-static inline CxIterator cxListMutBackwardsIterator(CxList *list) {
-    if (list == NULL) list = cxEmptyList;
-    return cxListMutBackwardsIteratorAt(list, list->collection.size - 1);
-}
+CX_EXPORT CxIterator cxListBackwardsIterator(const CxList *list);
 
 /**
  * Returns the index of the first element that equals @p elem.
@@ -1019,17 +874,11 @@
  * @see cxListIndexValid()
  * @see cxListContains()
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-static inline size_t cxListFind(
-        const CxList *list,
-        const void *elem
-) {
-    return list->cl->find_remove((CxList*)list, elem, false);
-}
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT size_t cxListFind(const CxList *list, const void *elem);
 
 /**
- * Checks, if the list contains the specified element.
+ * Checks if the list contains the specified element.
  *
  * The elements are compared with the list's comparator function.
  *
@@ -1039,14 +888,8 @@
  * @retval false if the element is not contained
  * @see cxListFind()
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-static inline bool cxListContains(
-    const CxList* list,
-    const void* elem
-) {
-    return list->cl->find_remove((CxList*)list, elem, false) < list->collection.size;
-}
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT bool cxListContains(const CxList* list, const void* elem);
 
 /**
  * Checks if the specified index is within bounds.
@@ -1056,11 +899,8 @@
  * @retval true if the index is within bounds
  * @retval false if the index is out of bounds
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-static inline bool cxListIndexValid(const CxList *list, size_t index) {
-    return index < list->collection.size;
-}
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT bool cxListIndexValid(const CxList *list, size_t index);
 
 /**
  * Removes and returns the index of the first element that equals @p elem.
@@ -1074,12 +914,7 @@
  * @see cxListIndexValid()
  */
 cx_attr_nonnull
-static inline size_t cxListFindRemove(
-        CxList *list,
-        const void *elem
-) {
-    return list->cl->find_remove(list, elem, true);
-}
+CX_EXPORT size_t cxListFindRemove(CxList *list, const void *elem);
 
 /**
  * Sorts the list.
@@ -1089,11 +924,7 @@
  * @param list the list
  */
 cx_attr_nonnull
-static inline void cxListSort(CxList *list) {
-    if (list->collection.sorted) return;
-    list->cl->sort(list);
-    list->collection.sorted = true;
-}
+CX_EXPORT void cxListSort(CxList *list);
 
 /**
  * Reverses the order of the items.
@@ -1101,11 +932,7 @@
  * @param list the list
  */
 cx_attr_nonnull
-static inline void cxListReverse(CxList *list) {
-    // still sorted, but not according to the cmp_func
-    list->collection.sorted = false;
-    list->cl->reverse(list);
-}
+CX_EXPORT void cxListReverse(CxList *list);
 
 /**
  * Compares a list to another list of the same type.
@@ -1116,28 +943,116 @@
  * @param list the list
  * @param other the list to compare to
  * @retval zero both lists are equal element wise
- * @retval negative the first list is smaller
+ * @retval negative the first list is smaller,
  * or the first non-equal element in the first list is smaller
  * @retval positive the first list is larger
  * or the first non-equal element in the first list is larger
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-int cxListCompare(
-        const CxList *list,
-        const CxList *other
-);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT int cxListCompare(const CxList *list, const CxList *other);
 
 /**
  * Deallocates the memory of the specified list structure.
  *
  * Also calls the content destructor functions for each element, if specified.
  *
- * @param list the list which shall be freed
+ * @param list the list that shall be freed
+ */
+CX_EXPORT void cxListFree(CxList *list);
+
+
+/**
+ * Performs a deep clone of one list into another.
+ *
+ * If the destination list already contains elements, the cloned elements
+ * are appended to that list.
+ *
+ * @attention If the cloned elements need to be destroyed by a destructor
+ * function, you must make sure that the destination list also uses this
+ * destructor function.
+ *
+ * @param dst the destination list
+ * @param src the source list
+ * @param clone_func the clone function for the elements
+ * @param clone_allocator the allocator that is passed to the clone function
+ * @param data optional additional data that is passed to the clone function
+ * @retval zero when all elements were successfully cloned
+ * @retval non-zero when an allocation error occurred
+ * @see cxListUnion()
+ */
+cx_attr_nonnull_arg(1, 2, 3)
+CX_EXPORT int cxListClone(CxList *dst, const CxList *src,
+        cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data);
+
+/**
+ * Clones elements from a list only if they are not present in another list.
+ *
+ * If the @p minuend does not contain duplicates, this is equivalent to adding
+ * the set difference to the destination list.
+ *
+ * This function is optimized for the case when both the @p minuend and the
+ * @p subtrahend are sorted.
+ *
+ * @param dst the destination list
+ * @param minuend the list to subtract elements from
+ * @param subtrahend the elements that shall be subtracted
+ * @param clone_func the clone function for the elements
+ * @param clone_allocator the allocator that is passed to the clone function
+ * @param data optional additional data that is passed to the clone function
+ * @retval zero when the elements were successfully cloned
+ * @retval non-zero when an allocation error occurred
  */
-cx_attr_export
-void cxListFree(CxList *list);
+cx_attr_nonnull_arg(1, 2, 3, 4)
+CX_EXPORT int cxListDifference(CxList *dst,
+        const CxList *minuend, const CxList *subtrahend,
+        cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data);
+
+/**
+ * Clones elements from a list only if they are also present in another list.
+ *
+ * This function is optimized for the case when both the @p src and the
+ * @p other list are sorted.
+ *
+ * If the destination list already contains elements, the intersection is appended
+ * to that list.
+ *
+ * @param dst the destination list
+ * @param src the list to clone the elements from
+ * @param other the list to check the elements for existence
+ * @param clone_func the clone function for the elements
+ * @param clone_allocator the allocator that is passed to the clone function
+ * @param data optional additional data that is passed to the clone function
+ * @retval zero when the elements were successfully cloned
+ * @retval non-zero when an allocation error occurred
+ */
+cx_attr_nonnull_arg(1, 2, 3, 4)
+CX_EXPORT int cxListIntersection(CxList *dst, const CxList *src, const CxList *other,
+        cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data);
+
+/**
+ * Performs a deep clone of one list into another, skipping duplicates.
+ *
+ * This function is optimized for the case when both the @p src and the
+ * @p other list are sorted.
+ * In that case, the union will also be sorted.
+ *
+ * If the destination list already contains elements, the union is appended
+ * to that list. In that case the destination is not necessarily sorted.
+ *
+ * @param dst the destination list
+ * @param src the primary source list
+ * @param other the other list, where elements are only cloned from
+ * when they are not in @p src
+ * @param clone_func the clone function for the elements
+ * @param clone_allocator the allocator that is passed to the clone function
+ * @param data optional additional data that is passed to the clone function
+ * @retval zero when the elements were successfully cloned
+ * @retval non-zero when an allocation error occurred
+ * @see cxListClone()
+ */
+cx_attr_nonnull_arg(1, 2, 3, 4)
+CX_EXPORT int cxListUnion(CxList *dst, const CxList *src, const CxList *other,
+        cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data);
 
 
 #ifdef __cplusplus
--- a/ucx/cx/map.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/cx/map.h	Sat Nov 08 23:06:11 2025 +0100
@@ -41,6 +41,11 @@
 #include "string.h"
 #include "hash_key.h"
 
+#ifndef UCX_LIST_H
+// forward-declare CxList
+typedef struct cx_list_s CxList;
+#endif
+
 #ifdef    __cplusplus
 extern "C" {
 #endif
@@ -111,16 +116,7 @@
     /**
      * Handle for the source map.
      */
-    union {
-        /**
-         * Access for mutating iterators.
-         */
-        CxMap *m;
-        /**
-         * Access for normal iterators.
-         */
-        const CxMap *c;
-    } map;
+    CxMap *map;
 
     /**
      * Handle for the current element.
@@ -189,19 +185,12 @@
      * shall only allocate memory instead of adding an existing value to the map.
      * Returns a pointer to the allocated memory or @c NULL if allocation fails.
      */
-    void *(*put)(
-            CxMap *map,
-            CxHashKey key,
-            void *value
-    );
+    void *(*put)(CxMap *map, CxHashKey key, void *value);
 
     /**
      * Returns an element.
      */
-    void *(*get)(
-            const CxMap *map,
-            CxHashKey key
-    );
+    void *(*get)(const CxMap *map, CxHashKey key);
 
     /**
      * Removes an element.
@@ -213,11 +202,7 @@
      * The function SHALL return zero when the @p key was found and
      * non-zero, otherwise. 
      */
-    int (*remove)(
-            CxMap *map,
-            CxHashKey key,
-            void *targetbuf
-    );
+    int (*remove)(CxMap *map, CxHashKey key, void *targetbuf);
 
     /**
      * Creates an iterator for this map.
@@ -230,11 +215,10 @@
  *
  * Writing to that map is not allowed.
  *
- * You can use this is a placeholder for initializing CxMap pointers
+ * You can use this as a placeholder for initializing CxMap pointers
  * for which you do not want to reserve memory right from the beginning.
  */
-cx_attr_export
-extern CxMap *const cxEmptyMap;
+CX_EXPORT extern CxMap *const cxEmptyMap;
 
 /**
  * Deallocates the memory of the specified map.
@@ -243,8 +227,7 @@
  *
  * @param map the map to be freed
  */
-cx_attr_export
-void cxMapFree(CxMap *map);
+CX_EXPORT void cxMapFree(CxMap *map);
 
 
 /**
@@ -255,9 +238,7 @@
  * @param map the map to be cleared
  */
 cx_attr_nonnull
-static inline void cxMapClear(CxMap *map) {
-    map->cl->clear(map);
-}
+CX_EXPORT void cxMapClear(CxMap *map);
 
 /**
  * Returns the number of elements in this map.
@@ -266,9 +247,7 @@
  * @return the number of stored elements
  */
 cx_attr_nonnull
-static inline size_t cxMapSize(const CxMap *map) {
-    return map->collection.size;
-}
+CX_EXPORT size_t cxMapSize(const CxMap *map);
 
 /**
  * Creates a value iterator for a map.
@@ -284,15 +263,12 @@
  * @return an iterator for the currently stored values
  */
 cx_attr_nodiscard
-static inline CxMapIterator cxMapIteratorValues(const CxMap *map) {
-    if (map == NULL) map = cxEmptyMap;
-    return map->cl->iterator(map, CX_MAP_ITERATOR_VALUES);
-}
+CX_EXPORT CxMapIterator cxMapIteratorValues(const CxMap *map);
 
 /**
  * Creates a key iterator for a map.
  *
- * The elements of the iterator are keys of type CxHashKey and the pointer returned
+ * The elements of the iterator are keys of type CxHashKey, and the pointer returned
  * during iterator shall be treated as @c const @c CxHashKey* .
  *
  * @note An iterator iterates over all elements successively. Therefore, the order
@@ -302,15 +278,12 @@
  * @return an iterator for the currently stored keys
  */
 cx_attr_nodiscard
-static inline CxMapIterator cxMapIteratorKeys(const CxMap *map) {
-    if (map == NULL) map = cxEmptyMap;
-    return map->cl->iterator(map, CX_MAP_ITERATOR_KEYS);
-}
+CX_EXPORT CxMapIterator cxMapIteratorKeys(const CxMap *map);
 
 /**
  * Creates an iterator for a map.
  *
- * The elements of the iterator are key/value pairs of type CxMapEntry and the pointer returned
+ * The elements of the iterator are key/value pairs of type CxMapEntry, and the pointer returned
  * during iterator shall be treated as @c const @c CxMapEntry* .
  *
  * @note An iterator iterates over all elements successively. Therefore, the order
@@ -322,296 +295,30 @@
  * @see cxMapIteratorValues()
  */
 cx_attr_nodiscard
-static inline CxMapIterator cxMapIterator(const CxMap *map) {
-    if (map == NULL) map = cxEmptyMap;
-    return map->cl->iterator(map, CX_MAP_ITERATOR_PAIRS);
-}
-
-
-/**
- * Creates a mutating iterator over the values of a map.
- *
- * When the map is storing pointers, those pointers are returned.
- * Otherwise, the iterator iterates over pointers to the memory within the map where the
- * respective elements are stored.
- *
- * @note An iterator iterates over all elements successively. Therefore, the order
- * highly depends on the map implementation and may change arbitrarily when the contents change.
- *
- * @param map the map to create the iterator for (can be @c NULL)
- * @return an iterator for the currently stored values
- */
-cx_attr_nodiscard
-cx_attr_export
-CxMapIterator cxMapMutIteratorValues(CxMap *map);
-
-/**
- * Creates a mutating iterator over the keys of a map.
- *
- * The elements of the iterator are keys of type CxHashKey and the pointer returned
- * during iterator shall be treated as @c const @c CxHashKey* .
- *
- * @note An iterator iterates over all elements successively. Therefore, the order
- * highly depends on the map implementation and may change arbitrarily when the contents change.
- *
- * @param map the map to create the iterator for (can be @c NULL)
- * @return an iterator for the currently stored keys
- */
-cx_attr_nodiscard
-cx_attr_export
-CxMapIterator cxMapMutIteratorKeys(CxMap *map);
+CX_EXPORT CxMapIterator cxMapIterator(const CxMap *map);
 
 /**
- * Creates a mutating iterator for a map.
+ * Puts a key/value-pair into the map.
  *
- * The elements of the iterator are key/value pairs of type CxMapEntry and the pointer returned
- * during iterator shall be treated as @c const @c CxMapEntry* .
- *
- * @note An iterator iterates over all elements successively. Therefore, the order
- * highly depends on the map implementation and may change arbitrarily when the contents change.
+ * A possible existing value will be overwritten.
+ * If destructor functions are specified, they are called for
+ * the overwritten element.
  *
- * @param map the map to create the iterator for (can be @c NULL)
- * @return an iterator for the currently stored entries
- * @see cxMapMutIteratorKeys()
- * @see cxMapMutIteratorValues()
- */
-cx_attr_nodiscard
-cx_attr_export
-CxMapIterator cxMapMutIterator(CxMap *map);
-
-#ifdef __cplusplus
-} // end the extern "C" block here, because we want to start overloading
-cx_attr_nonnull
-static inline int cxMapPut(
-        CxMap *map,
-        CxHashKey const &key,
-        void *value
-) {
-    return map->cl->put(map, key, value) == NULL;
-}
-
-cx_attr_nonnull
-static inline int cxMapPut(
-        CxMap *map,
-        cxstring const &key,
-        void *value
-) {
-    return map->cl->put(map, cx_hash_key_cxstr(key), value) == NULL;
-}
-
-cx_attr_nonnull
-static inline int cxMapPut(
-        CxMap *map,
-        cxmutstr const &key,
-        void *value
-) {
-    return map->cl->put(map, cx_hash_key_cxstr(key), value) == NULL;
-}
-
-cx_attr_nonnull
-cx_attr_cstr_arg(2)
-static inline int cxMapPut(
-        CxMap *map,
-        const char *key,
-        void *value
-) {
-    return map->cl->put(map, cx_hash_key_str(key), value) == NULL;
-}
-
-cx_attr_nonnull
-static inline void *cxMapEmplace(
-        CxMap *map,
-        CxHashKey const &key
-) {
-    return map->cl->put(map, key, NULL);
-}
-
-cx_attr_nonnull
-static inline void *cxMapEmplace(
-        CxMap *map,
-        cxstring const &key
-) {
-    return map->cl->put(map, cx_hash_key_cxstr(key), NULL);
-}
-
-cx_attr_nonnull
-static inline void *cxMapEmplace(
-        CxMap *map,
-        cxmutstr const &key
-) {
-    return map->cl->put(map, cx_hash_key_cxstr(key), NULL);
-}
-
-cx_attr_nonnull
-cx_attr_cstr_arg(2)
-static inline void *cxMapEmplace(
-        CxMap *map,
-        const char *key
-) {
-    return map->cl->put(map, cx_hash_key_str(key), NULL);
-}
-
-cx_attr_nonnull
-cx_attr_nodiscard
-static inline void *cxMapGet(
-        const CxMap *map,
-        CxHashKey const &key
-) {
-    return map->cl->get(map, key);
-}
-
-cx_attr_nonnull
-cx_attr_nodiscard
-static inline void *cxMapGet(
-        const CxMap *map,
-        cxstring const &key
-) {
-    return map->cl->get(map, cx_hash_key_cxstr(key));
-}
-
-cx_attr_nonnull
-cx_attr_nodiscard
-static inline void *cxMapGet(
-        const CxMap *map,
-        cxmutstr const &key
-) {
-    return map->cl->get(map, cx_hash_key_cxstr(key));
-}
-
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_cstr_arg(2)
-static inline void *cxMapGet(
-        const CxMap *map,
-        const char *key
-) {
-    return map->cl->get(map, cx_hash_key_str(key));
-}
-
-cx_attr_nonnull
-static inline int cxMapRemove(
-        CxMap *map,
-        CxHashKey const &key
-) {
-    return map->cl->remove(map, key, nullptr);
-}
-
-cx_attr_nonnull
-static inline int cxMapRemove(
-        CxMap *map,
-        cxstring const &key
-) {
-    return map->cl->remove(map, cx_hash_key_cxstr(key), nullptr);
-}
-
-cx_attr_nonnull
-static inline int cxMapRemove(
-        CxMap *map,
-        cxmutstr const &key
-) {
-    return map->cl->remove(map, cx_hash_key_cxstr(key), nullptr);
-}
-
-cx_attr_nonnull
-cx_attr_cstr_arg(2)
-static inline int cxMapRemove(
-        CxMap *map,
-        const char *key
-) {
-    return map->cl->remove(map, cx_hash_key_str(key), nullptr);
-}
-
-cx_attr_nonnull
-cx_attr_access_w(3)
-static inline int cxMapRemoveAndGet(
-        CxMap *map,
-        CxHashKey key,
-        void *targetbuf
-) {
-    return map->cl->remove(map, key, targetbuf);
-}
-
-cx_attr_nonnull
-cx_attr_access_w(3)
-static inline int cxMapRemoveAndGet(
-        CxMap *map,
-        cxstring key,
-        void *targetbuf
-) {
-    return map->cl->remove(map, cx_hash_key_cxstr(key), targetbuf);
-}
-
-cx_attr_nonnull
-cx_attr_access_w(3)
-static inline int cxMapRemoveAndGet(
-        CxMap *map,
-        cxmutstr key,
-        void *targetbuf
-) {
-    return map->cl->remove(map, cx_hash_key_cxstr(key), targetbuf);
-}
-
-cx_attr_nonnull
-cx_attr_access_w(3)
-cx_attr_cstr_arg(2)
-static inline int cxMapRemoveAndGet(
-        CxMap *map,
-        const char *key,
-        void *targetbuf
-) {
-    return map->cl->remove(map, cx_hash_key_str(key), targetbuf);
-}
-
-#else // __cplusplus
-
-/**
- * @copydoc cxMapPut()
+ * If this map is storing pointers, the @p value pointer is written
+ * to the map. Otherwise, the memory is copied from @p value with
+ * memcpy().
+ *
+ * The @p key is always copied.
+ *
+ * @param map the map
+ * @param key the key
+ * @param value the value
+ * @retval zero success
+ * @retval non-zero value on memory allocation failure
+ * @see cxMapPut()
  */
 cx_attr_nonnull
-static inline int cx_map_put(
-        CxMap *map,
-        CxHashKey key,
-        void *value
-) {
-    return map->cl->put(map, key, value) == NULL;
-}
-
-/**
- * @copydoc cxMapPut()
- */
-cx_attr_nonnull
-static inline int cx_map_put_cxstr(
-        CxMap *map,
-        cxstring key,
-        void *value
-) {
-    return map->cl->put(map, cx_hash_key_cxstr(key), value) == NULL;
-}
-
-/**
- * @copydoc cxMapPut()
- */
-cx_attr_nonnull
-static inline int cx_map_put_mustr(
-        CxMap *map,
-        cxmutstr key,
-        void *value
-) {
-    return map->cl->put(map, cx_hash_key_cxstr(key), value) == NULL;
-}
-
-/**
- * @copydoc cxMapPut()
- */
-cx_attr_nonnull
-cx_attr_cstr_arg(2)
-static inline int cx_map_put_str(
-        CxMap *map,
-        const char *key,
-        void *value
-) {
-    return map->cl->put(map, cx_hash_key_str(key), value) == NULL;
-}
+CX_EXPORT int cx_map_put(CxMap *map, CxHashKey key, void *value);
 
 /**
  * Puts a key/value-pair into the map.
@@ -627,63 +334,35 @@
  * The @p key is always copied.
  *
  * @param map (@c CxMap*) the map
- * @param key (@c CxHashKey, @c char*, @c cxstring, or @c cxmutstr) the key
+ * @param key (any supported key type) the key
  * @param value (@c void*) the value
  * @retval zero success
  * @retval non-zero value on memory allocation failure
+ * @see CX_HASH_KEY()
  */
-#define cxMapPut(map, key, value) _Generic((key), \
-    CxHashKey: cx_map_put,                        \
-    cxstring: cx_map_put_cxstr,                   \
-    cxmutstr: cx_map_put_mustr,                   \
-    char*: cx_map_put_str,                        \
-    const char*: cx_map_put_str)                  \
-    (map, key, value)
+#define cxMapPut(map, key, value) cx_map_put(map, CX_HASH_KEY(key), value)
 
 /**
- * @copydoc cxMapEmplace()
- */
-cx_attr_nonnull
-static inline void *cx_map_emplace(
-        CxMap *map,
-        CxHashKey key
-) {
-    return map->cl->put(map, key, NULL);
-}
-
-/**
- * @copydoc cxMapEmplace()
+ * Allocates memory for a value in the map associated with the specified key.
+ *
+ * A possible existing value will be overwritten.
+ * If destructor functions are specified, they are called for
+ * the overwritten element.
+ *
+ * If the map is storing pointers, this function returns a @c void** pointer,
+ * meaning a pointer to that pointer.
+ *
+ * The @p key is always copied.
+ *
+ * @param map the map
+ * @param key the key
+ * @return the pointer to the allocated memory or @c NULL if allocation fails
+ * @retval zero success
+ * @retval non-zero value on memory allocation failure
+ * @see cxMapEmplace()
  */
 cx_attr_nonnull
-static inline void *cx_map_emplace_cxstr(
-        CxMap *map,
-        cxstring key
-) {
-    return map->cl->put(map, cx_hash_key_cxstr(key), NULL);
-}
-
-/**
- * @copydoc cxMapEmplace()
- */
-cx_attr_nonnull
-static inline void *cx_map_emplace_mustr(
-        CxMap *map,
-        cxmutstr key
-) {
-    return map->cl->put(map, cx_hash_key_cxstr(key), NULL);
-}
-
-/**
- * @copydoc cxMapEmplace()
- */
-cx_attr_nonnull
-cx_attr_cstr_arg(2)
-static inline void *cx_map_emplace_str(
-        CxMap *map,
-        const char *key
-) {
-    return map->cl->put(map, cx_hash_key_str(key), NULL);
-}
+CX_EXPORT void *cx_map_emplace(CxMap *map, CxHashKey key);
 
 /**
  * Allocates memory for a value in the map associated with the specified key.
@@ -698,67 +377,28 @@
  * The @p key is always copied.
  *
  * @param map (@c CxMap*) the map
- * @param key (@c CxHashKey, @c char*, @c cxstring, or @c cxmutstr) the key
+ * @param key (any supported key type) the key
  * @return the pointer to the allocated memory or @c NULL if allocation fails
  * @retval zero success
  * @retval non-zero value on memory allocation failure
+ * @see CX_HASH_KEY()
  */
-#define cxMapEmplace(map, key) _Generic((key), \
-    CxHashKey: cx_map_emplace,                 \
-    cxstring: cx_map_emplace_cxstr,            \
-    cxmutstr: cx_map_emplace_mustr,            \
-    char*: cx_map_emplace_str,                 \
-    const char*: cx_map_emplace_str)           \
-    (map, key)
-
-/**
- * @copydoc cxMapGet()
- */
-cx_attr_nonnull
-cx_attr_nodiscard
-static inline void *cx_map_get(
-        const CxMap *map,
-        CxHashKey key
-) {
-    return map->cl->get(map, key);
-}
+#define cxMapEmplace(map, key) cx_map_emplace(map, CX_HASH_KEY(key))
 
 /**
- * @copydoc cxMapGet()
- */
-cx_attr_nonnull
-cx_attr_nodiscard
-static inline void *cx_map_get_cxstr(
-        const CxMap *map,
-        cxstring key
-) {
-    return map->cl->get(map, cx_hash_key_cxstr(key));
-}
-
-/**
- * @copydoc cxMapGet()
+ * Retrieves a value by using a key.
+ *
+ * If this map is storing pointers, the stored pointer is returned.
+ * Otherwise, a pointer to the element within the map's memory
+ * is returned (which is valid as long as the element stays in the map).
+ *
+ * @param map the map
+ * @param key the key
+ * @return the value
+ * @see cxMapGet()
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-static inline void *cx_map_get_mustr(
-        const CxMap *map,
-        cxmutstr key
-) {
-    return map->cl->get(map, cx_hash_key_cxstr(key));
-}
-
-/**
- * @copydoc cxMapGet()
- */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_cstr_arg(2)
-static inline void *cx_map_get_str(
-        const CxMap *map,
-        const char *key
-) {
-    return map->cl->get(map, cx_hash_key_str(key));
-}
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT void *cx_map_get(const CxMap *map, CxHashKey key);
 
 /**
  * Retrieves a value by using a key.
@@ -768,134 +408,55 @@
  * is returned (which is valid as long as the element stays in the map).
  *
  * @param map (@c CxMap*) the map
- * @param key (@c CxHashKey, @c char*, @c cxstring, or @c cxmutstr) the key
- * @return (@c void*) the value
+ * @param key (any supported key type) the key
+ * @return (@c void*) the value or @c NULL when no value with that @p key exists
+ * @see CX_HASH_KEY()
  */
-#define cxMapGet(map, key) _Generic((key), \
-    CxHashKey: cx_map_get,                 \
-    cxstring: cx_map_get_cxstr,            \
-    cxmutstr: cx_map_get_mustr,            \
-    char*: cx_map_get_str,                 \
-    const char*: cx_map_get_str)           \
-    (map, key)
-
-/**
- * @copydoc cxMapRemove()
- */
-cx_attr_nonnull
-static inline int cx_map_remove(
-        CxMap *map,
-        CxHashKey key
-) {
-    return map->cl->remove(map, key, NULL);
-}
+#define cxMapGet(map, key) cx_map_get(map, CX_HASH_KEY(key))
 
 /**
- * @copydoc cxMapRemove()
- */
-cx_attr_nonnull
-static inline int cx_map_remove_cxstr(
-        CxMap *map,
-        cxstring key
-) {
-    return map->cl->remove(map, cx_hash_key_cxstr(key), NULL);
-}
-
-/**
- * @copydoc cxMapRemove()
+ * Checks if a map contains a specific key.
+ *
+ * @param map (@c CxMap*) the map
+ * @param key (any supported key type) the key
+ * @retval true if the key exists in the map
+ * @retval false if the key does not exist in the map
+ * @see CX_HASH_KEY()
  */
-cx_attr_nonnull
-static inline int cx_map_remove_mustr(
-        CxMap *map,
-        cxmutstr key
-) {
-    return map->cl->remove(map, cx_hash_key_cxstr(key), NULL);
-}
-
-/**
- * @copydoc cxMapRemove()
- */
-cx_attr_nonnull
-cx_attr_cstr_arg(2)
-static inline int cx_map_remove_str(
-        CxMap *map,
-        const char *key
-) {
-    return map->cl->remove(map, cx_hash_key_str(key), NULL);
-}
+#define cxMapContains(map, key) (cxMapGet(map, key) != NULL)
 
 /**
  * Removes a key/value-pair from the map by using the key.
  *
- * Always invokes the destructors functions, if any, on the removed element.
+ * Invokes the destructor functions, if any, on the removed element if and only if the
+ * @p targetbuf is @c NULL.
+ *
+ * @param map the map
+ * @param key the key
+ * @param targetbuf the optional buffer where the removed element shall be copied to
+ * @retval zero success
+ * @retval non-zero the key was not found
+ *
+ * @see cxMapRemove()
+ * @see cxMapRemoveAndGet()
+ */
+cx_attr_nonnull_arg(1)
+CX_EXPORT int cx_map_remove(CxMap *map, CxHashKey key, void *targetbuf);
+
+/**
+ * Removes a key/value-pair from the map by using the key.
+ *
+ * Always invokes the destructor functions, if any, on the removed element.
  *
  * @param map (@c CxMap*) the map
- * @param key (@c CxHashKey, @c char*, @c cxstring, or @c cxmutstr) the key
+ * @param key (any supported key type) the key
  * @retval zero success
  * @retval non-zero the key was not found
  * 
  * @see cxMapRemoveAndGet()
- */
-#define cxMapRemove(map, key) _Generic((key), \
-    CxHashKey: cx_map_remove,                 \
-    cxstring: cx_map_remove_cxstr,            \
-    cxmutstr: cx_map_remove_mustr,            \
-    char*: cx_map_remove_str,                 \
-    const char*: cx_map_remove_str)           \
-    (map, key)
-
-/**
- * @copydoc cxMapRemoveAndGet()
- */
-cx_attr_nonnull
-cx_attr_access_w(3)
-static inline int cx_map_remove_and_get(
-        CxMap *map,
-        CxHashKey key,
-        void *targetbuf
-) {
-    return map->cl->remove(map, key, targetbuf);
-}
-
-/**
- * @copydoc cxMapRemoveAndGet()
+ * @see CX_HASH_KEY()
  */
-cx_attr_nonnull
-cx_attr_access_w(3)
-static inline int cx_map_remove_and_get_cxstr(
-        CxMap *map,
-        cxstring key,
-        void *targetbuf
-) {
-    return map->cl->remove(map, cx_hash_key_cxstr(key), targetbuf);
-}
-
-/**
- * @copydoc cxMapRemoveAndGet()
- */
-cx_attr_nonnull
-cx_attr_access_w(3)
-static inline int cx_map_remove_and_get_mustr(
-        CxMap *map,
-        cxmutstr key,
-        void *targetbuf
-) {
-    return map->cl->remove(map, cx_hash_key_cxstr(key), targetbuf);
-}
-
-/**
- * @copydoc cxMapRemoveAndGet()
- */
-cx_attr_nonnull
-cx_attr_access_w(3)
-cx_attr_cstr_arg(2)
-static inline int cx_map_remove_and_get_str(
-        CxMap *map,
-        const char *key,
-        void *targetbuf
-) {
-    return map->cl->remove(map, cx_hash_key_str(key), targetbuf);
-}
+#define cxMapRemove(map, key) cx_map_remove(map, CX_HASH_KEY(key), NULL)
 
 /**
  * Removes a key/value-pair from the map by using the key.
@@ -909,21 +470,141 @@
  * and not the object it points to.
  *
  * @param map (@c CxMap*) the map
- * @param key (@c CxHashKey, @c char*, @c cxstring, or @c cxmutstr) the key
+ * @param key (any supported key type) the key
  * @param targetbuf (@c void*) the buffer where the element shall be copied to
  * @retval zero success
  * @retval non-zero the key was not found
  *
  * @see cxMapRemove()
+ * @see CX_HASH_KEY()
  */
-#define cxMapRemoveAndGet(map, key, targetbuf) _Generic((key), \
-    CxHashKey: cx_map_remove_and_get,               \
-    cxstring: cx_map_remove_and_get_cxstr,          \
-    cxmutstr: cx_map_remove_and_get_mustr,          \
-    char*: cx_map_remove_and_get_str,               \
-    const char*: cx_map_remove_and_get_str)         \
-    (map, key, targetbuf)
+#define cxMapRemoveAndGet(map, key, targetbuf) cx_map_remove(map, CX_HASH_KEY(key), targetbuf)
+
+
+/**
+ * Performs a deep clone of one map into another.
+ *
+ * If the destination map already contains entries, the cloned entries
+ * are added to that map, possibly overwriting existing elements when
+ * the keys already exist.
+ *
+ * When elements in the destination map need to be replaced, any destructor
+ * function is called on the replaced elements before replacing them.
+ *
+ * @attention If the cloned elements need to be destroyed by a destructor
+ * function, you must make sure that the destination map also uses this
+ * destructor function.
+ *
+ * @param dst the destination map
+ * @param src the source map
+ * @param clone_func the clone function for the values
+ * @param clone_allocator the allocator that is passed to the clone function
+ * @param data optional additional data that is passed to the clone function
+ * @retval zero when all elements were successfully cloned
+ * @retval non-zero when an allocation error occurred
+ */
+cx_attr_nonnull_arg(1, 2, 3)
+CX_EXPORT int cxMapClone(CxMap *dst, const CxMap *src,
+        cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data);
+
+
+/**
+ * Clones entries of a map if their key is not present in another map.
+ *
+ * @param dst the destination map
+ * @param minuend the map to subtract the entries from
+ * @param subtrahend the map containing the elements to be subtracted
+ * @param clone_func the clone function for the values
+ * @param clone_allocator the allocator that is passed to the clone function
+ * @param data optional additional data that is passed to the clone function
+ * @retval zero when the elements were successfully cloned
+ * @retval non-zero when an allocation error occurred
+ */
+cx_attr_nonnull_arg(1, 2, 3, 4)
+CX_EXPORT int cxMapDifference(CxMap *dst, const CxMap *minuend, const CxMap *subtrahend,
+        cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data);
 
-#endif // __cplusplus
+/**
+ * Clones entries of a map if their key is not present in a list.
+ *
+ * Note that the list must contain keys of type @c CxKey
+ * (or pointers to such keys) and must use @c cx_hash_key_cmp
+ * as the compare function.
+ * Generic key types cannot be processed in this case.
+ *
+ * @param dst the destination map
+ * @param src the source map
+ * @param keys the list of @c CxKey items
+ * @param clone_func the clone function for the values
+ * @param clone_allocator the allocator that is passed to the clone function
+ * @param data optional additional data that is passed to the clone function
+ * @retval zero when the elements were successfully cloned
+ * @retval non-zero when an allocation error occurred
+ */
+cx_attr_nonnull_arg(1, 2, 3, 4)
+CX_EXPORT int cxMapListDifference(CxMap *dst, const CxMap *src, const CxList *keys,
+        cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data);
+
+
+/**
+ * Clones entries of a map only if their key is present in another map.
+ *
+ * @param dst the destination map
+ * @param src the map to clone the entries from
+ * @param other the map to check for existence of the keys
+ * @param clone_func the clone function for the values
+ * @param clone_allocator the allocator that is passed to the clone function
+ * @param data optional additional data that is passed to the clone function
+ * @retval zero when the elements were successfully cloned
+ * @retval non-zero when an allocation error occurred
+ */
+cx_attr_nonnull_arg(1, 2, 3, 4)
+CX_EXPORT int cxMapIntersection(CxMap *dst, const CxMap *src, const CxMap *other,
+        cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data);
+
+/**
+ * Clones entries of a map only if their key is present in a list.
+ *
+ * Note that the list must contain keys of type @c CxKey
+ * (or pointers to such keys) and must use @c cx_hash_key_cmp
+ * as the compare function.
+ * Generic key types cannot be processed in this case.
+ *
+ * @param dst the destination map
+ * @param src the source map
+ * @param keys the list of @c CxKey items
+ * @param clone_func the clone function for the values
+ * @param clone_allocator the allocator that is passed to the clone function
+ * @param data optional additional data that is passed to the clone function
+ * @retval zero when the elements were successfully cloned
+ * @retval non-zero when an allocation error occurred
+ */
+cx_attr_nonnull_arg(1, 2, 3, 4)
+CX_EXPORT int cxMapListIntersection(CxMap *dst, const CxMap *src, const CxList *keys,
+        cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data);
+
+/**
+ * Clones entries into a map if their key does not exist yet.
+ *
+ * If you want to calculate the union of two maps into a fresh new map,
+ * you can proceed as follows:
+ * 1. Clone the first map into a fresh, empty map.
+ * 2. Use this function to clone the second map into the result from step 1.
+ *
+ * @param dst the destination map
+ * @param src the map to clone the entries from
+ * @param clone_func the clone function for the values
+ * @param clone_allocator the allocator that is passed to the clone function
+ * @param data optional additional data that is passed to the clone function
+ * @retval zero when the elements were successfully cloned
+ * @retval non-zero when an allocation error occurred
+ */
+cx_attr_nonnull_arg(1, 2, 3)
+CX_EXPORT int cxMapUnion(CxMap *dst, const CxMap *src,
+        cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data);
+
+#ifdef    __cplusplus
+} // extern "C"
+#endif
 
 #endif // UCX_MAP_H
--- a/ucx/cx/mempool.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/cx/mempool.h	Sat Nov 08 23:06:11 2025 +0100
@@ -156,8 +156,7 @@
  *
  * @param pool the memory pool to free
  */
-cx_attr_export
-void cxMempoolFree(CxMempool *pool);
+CX_EXPORT void cxMempoolFree(CxMempool *pool);
 
 /**
  * Creates an array-based memory pool.
@@ -169,11 +168,8 @@
  * @param type the type of memory pool
  * @return the created memory pool or @c NULL if allocation failed
  */
-cx_attr_nodiscard
-cx_attr_malloc
-cx_attr_dealloc(cxMempoolFree, 1)
-cx_attr_export
-CxMempool *cxMempoolCreate(size_t capacity, enum cx_mempool_type type);
+cx_attr_nodiscard cx_attr_malloc cx_attr_dealloc(cxMempoolFree, 1)
+CX_EXPORT CxMempool *cxMempoolCreate(size_t capacity, enum cx_mempool_type type);
 
 /**
  * Creates a basic array-based memory pool.
@@ -212,8 +208,7 @@
  * @param fnc the destructor that shall be applied to all memory blocks
  */
 cx_attr_nonnull_arg(1)
-cx_attr_export
-void cxMempoolGlobalDestructor(CxMempool *pool, cx_destructor_func fnc);
+CX_EXPORT void cxMempoolGlobalDestructor(CxMempool *pool, cx_destructor_func fnc);
 
 /**
  * Sets the global destructor for all memory blocks within the specified pool.
@@ -223,8 +218,7 @@
  * @param data additional data for the destructor function
  */
 cx_attr_nonnull_arg(1)
-cx_attr_export
-void cxMempoolGlobalDestructor2(CxMempool *pool, cx_destructor_func2 fnc, void *data);
+CX_EXPORT void cxMempoolGlobalDestructor2(CxMempool *pool, cx_destructor_func2 fnc, void *data);
 
 /**
  * Sets the destructor function for a specific allocated memory object.
@@ -237,11 +231,7 @@
  * @param fnc the destructor function
  */
 cx_attr_nonnull
-cx_attr_export
-void cxMempoolSetDestructor(
-        void *memory,
-        cx_destructor_func fnc
-);
+CX_EXPORT void cxMempoolSetDestructor(void *memory, cx_destructor_func fnc);
 
 /**
  * Sets the destructor function for a specific allocated memory object.
@@ -255,12 +245,7 @@
  * @param data additional data for the destructor function
  */
 cx_attr_nonnull
-cx_attr_export
-void cxMempoolSetDestructor2(
-        void *memory,
-        cx_destructor_func2 fnc,
-        void *data
-);
+CX_EXPORT void cxMempoolSetDestructor2(void *memory, cx_destructor_func2 fnc, void *data);
 
 /**
  * Removes the destructor function for a specific allocated memory object.
@@ -271,8 +256,7 @@
  * @param memory the object allocated in the pool
  */
 cx_attr_nonnull
-cx_attr_export
-void cxMempoolRemoveDestructor(void *memory);
+CX_EXPORT void cxMempoolRemoveDestructor(void *memory);
 
 /**
  * Removes the destructor function for a specific allocated memory object.
@@ -283,8 +267,7 @@
  * @param memory the object allocated in the pool
  */
 cx_attr_nonnull
-cx_attr_export
-void cxMempoolRemoveDestructor2(void *memory);
+CX_EXPORT void cxMempoolRemoveDestructor2(void *memory);
 
 /**
  * Registers foreign memory with this pool.
@@ -302,12 +285,7 @@
  * @retval non-zero failure
  */
 cx_attr_nonnull
-cx_attr_export
-int cxMempoolRegister(
-        CxMempool *pool,
-        void *memory,
-        cx_destructor_func destr
-);
+CX_EXPORT int cxMempoolRegister(CxMempool *pool, void *memory, cx_destructor_func destr);
 
 
 /**
@@ -330,13 +308,7 @@
  * @retval non-zero failure
  */
 cx_attr_nonnull
-cx_attr_export
-int cxMempoolRegister2(
-        CxMempool *pool,
-        void *memory,
-        cx_destructor_func2 destr,
-        void *data
-);
+CX_EXPORT int cxMempoolRegister2(CxMempool *pool, void *memory, cx_destructor_func2 destr, void *data);
 
 /**
  * Transfers all the memory managed by one pool to another.
@@ -354,11 +326,7 @@
  * @retval non-zero allocation failure or incompatible pools
  */
 cx_attr_nonnull
-cx_attr_export
-int cxMempoolTransfer(
-        CxMempool *source,
-        CxMempool *dest
-);
+CX_EXPORT int cxMempoolTransfer(CxMempool *source, CxMempool *dest);
 
 /**
  * Transfers an object from one pool to another.
@@ -375,12 +343,7 @@
  * @retval non-zero failure, or the object was not found in the source pool, or the pools are incompatible
  */
 cx_attr_nonnull
-cx_attr_export
-int cxMempoolTransferObject(
-        CxMempool *source,
-        CxMempool *dest,
-        const void *obj
-);
+CX_EXPORT int cxMempoolTransferObject(CxMempool *source, CxMempool *dest, const void *obj);
 
 #ifdef __cplusplus
 } // extern "C"
--- a/ucx/cx/printf.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/cx/printf.h	Sat Nov 08 23:06:11 2025 +0100
@@ -27,7 +27,7 @@
  */
 /**
  * @file printf.h
- * @brief Wrapper for write functions with a printf-like interface.
+ * @brief Wrapper for write-functions with a printf-like interface.
  * @author Mike Becker
  * @author Olaf Wintermann
  * @copyright 2-Clause BSD License
@@ -56,8 +56,7 @@
 /**
  * The maximum string length that fits into stack memory.
  */
-cx_attr_export
-extern const unsigned cx_printf_sbo_size;
+CX_EXPORT extern const unsigned cx_printf_sbo_size;
 
 /**
  * A @c fprintf like function which writes the output to a stream by
@@ -69,16 +68,8 @@
  * @param ... additional arguments
  * @return the total number of bytes written or an error code from stdlib printf implementation
  */
-cx_attr_nonnull_arg(1, 2, 3)
-cx_attr_printf(3, 4)
-cx_attr_cstr_arg(3)
-cx_attr_export
-int cx_fprintf(
-        void *stream,
-        cx_write_func wfc,
-        const char *fmt,
-        ...
-);
+cx_attr_nonnull_arg(1, 2, 3) cx_attr_printf(3, 4) cx_attr_cstr_arg(3)
+CX_EXPORT int cx_fprintf(void *stream, cx_write_func wfc, const char *fmt, ...);
 
 /**
  * A @c vfprintf like function which writes the output to a stream by
@@ -91,18 +82,11 @@
  * @return the total number of bytes written or an error code from stdlib printf implementation
  * @see cx_fprintf()
  */
-cx_attr_nonnull
-cx_attr_cstr_arg(3)
-cx_attr_export
-int cx_vfprintf(
-        void *stream,
-        cx_write_func wfc,
-        const char *fmt,
-        va_list ap
-);
+cx_attr_nonnull cx_attr_cstr_arg(3)
+CX_EXPORT int cx_vfprintf(void *stream, cx_write_func wfc, const char *fmt, va_list ap);
 
 /**
- * A @c asprintf like function which allocates space for a string
+ * An @c asprintf like function which allocates space for a string
  * the result is written to.
  *
  * @note The resulting string is guaranteed to be zero-terminated,
@@ -115,18 +99,11 @@
  * @return the formatted string
  * @see cx_strfree_a()
  */
-cx_attr_nonnull_arg(1, 2)
-cx_attr_printf(2, 3)
-cx_attr_cstr_arg(2)
-cx_attr_export
-cxmutstr cx_asprintf_a(
-        const CxAllocator *allocator,
-        const char *fmt,
-        ...
-);
+cx_attr_nonnull_arg(1, 2) cx_attr_printf(2, 3) cx_attr_cstr_arg(2)
+CX_EXPORT cxmutstr cx_asprintf_a(const CxAllocator *allocator, const char *fmt, ...);
 
 /**
- * A @c asprintf like function which allocates space for a string
+ * An @c asprintf like function which allocates space for a string
  * the result is written to.
  *
  * @note The resulting string is guaranteed to be zero-terminated,
@@ -138,8 +115,7 @@
  * @return (@c cxmutstr) the formatted string
  * @see cx_strfree()
  */
-#define cx_asprintf(fmt, ...) \
-    cx_asprintf_a(cxDefaultAllocator, fmt, __VA_ARGS__)
+#define cx_asprintf(fmt, ...) cx_asprintf_a(cxDefaultAllocator, fmt, __VA_ARGS__)
 
 /**
 * A @c vasprintf like function which allocates space for a string
@@ -155,21 +131,15 @@
  * @return the formatted string
  * @see cx_asprintf_a()
  */
-cx_attr_nonnull
-cx_attr_cstr_arg(2)
-cx_attr_export
-cxmutstr cx_vasprintf_a(
-        const CxAllocator *allocator,
-        const char *fmt,
-        va_list ap
-);
+cx_attr_nonnull cx_attr_cstr_arg(2)
+CX_EXPORT cxmutstr cx_vasprintf_a(const CxAllocator *allocator, const char *fmt, va_list ap);
 
 /**
 * A @c vasprintf like function which allocates space for a string
  * the result is written to.
  *
  * @note The resulting string is guaranteed to be zero-terminated,
- * unless there was in error, in which case the string's pointer
+ * unless there was an error, in which case the string's pointer
  * will be @c NULL.
  *
  * @param fmt (@c char*) format string
@@ -189,8 +159,7 @@
  * @see cx_fprintf()
  * @see cxBufferWrite()
  */
-#define cx_bprintf(buffer, fmt, ...) cx_fprintf((void*)buffer, \
-    cxBufferWriteFunc, fmt, __VA_ARGS__)
+#define cx_bprintf(buffer, fmt, ...) cx_fprintf((void*)buffer, cxBufferWriteFunc, fmt, __VA_ARGS__)
 
 
 /**
@@ -204,7 +173,7 @@
  * @param len (@c size_t*) a pointer to the length of the buffer
  * @param fmt (@c char*) the format string
  * @param ... additional arguments
- * @return (@c int) the length of produced string or an error code from stdlib printf implementation
+ * @return (@c int) the length of the produced string or an error code from stdlib printf implementation
  */
 #define cx_sprintf(str, len, fmt, ...) cx_sprintf_a(cxDefaultAllocator, str, len, fmt, __VA_ARGS__)
 
@@ -222,19 +191,10 @@
  * @param len a pointer to the length of the buffer
  * @param fmt the format string
  * @param ... additional arguments
- * @return the length of produced string or an error code from stdlib printf implementation
+ * @return the length of the produced string or an error code from stdlib printf implementation
  */
-cx_attr_nonnull_arg(1, 2, 3, 4)
-cx_attr_printf(4, 5)
-cx_attr_cstr_arg(4)
-cx_attr_export
-int cx_sprintf_a(
-        const CxAllocator *alloc,
-        char **str,
-        size_t *len,
-        const char *fmt,
-        ...
-);
+cx_attr_nonnull_arg(1, 2, 3, 4) cx_attr_printf(4, 5) cx_attr_cstr_arg(4)
+CX_EXPORT int cx_sprintf_a(const CxAllocator *alloc, char **str, size_t *len, const char *fmt, ...);
 
 
 /**
@@ -248,7 +208,7 @@
  * @param len (@c size_t*) a pointer to the length of the buffer
  * @param fmt (@c char*) the format string
  * @param ap (@c va_list) argument list
- * @return (@c int) the length of produced string or an error code from stdlib printf implementation
+ * @return (@c int) the length of the produced string or an error code from stdlib printf implementation
  */
 #define cx_vsprintf(str, len, fmt, ap) cx_vsprintf_a(cxDefaultAllocator, str, len, fmt, ap)
 
@@ -266,20 +226,10 @@
  * @param len a pointer to the length of the buffer
  * @param fmt the format string
  * @param ap argument list
- * @return the length of produced string or an error code from stdlib printf implementation
+ * @return the length of the produced string or an error code from stdlib printf implementation
  */
-cx_attr_nonnull
-cx_attr_cstr_arg(4)
-cx_attr_access_rw(2)
-cx_attr_access_rw(3)
-cx_attr_export
-int cx_vsprintf_a(
-        const CxAllocator *alloc,
-        char **str,
-        size_t *len,
-        const char *fmt,
-        va_list ap
-);
+cx_attr_nonnull  cx_attr_cstr_arg(4) cx_attr_access_rw(2) cx_attr_access_rw(3)
+CX_EXPORT int cx_vsprintf_a(const CxAllocator *alloc, char **str, size_t *len, const char *fmt, va_list ap);
 
 
 /**
@@ -300,7 +250,7 @@
  * @param str (@c char**) a pointer where the location of the result shall be stored
  * @param fmt (@c char*) the format string
  * @param ... additional arguments
- * @return (@c int) the length of produced string or an error code from stdlib printf implementation
+ * @return (@c int) the length of the produced string or an error code from stdlib printf implementation
  */
 #define cx_sprintf_s(buf, len, str, fmt, ...) cx_sprintf_sa(cxDefaultAllocator, buf, len, str, fmt, __VA_ARGS__)
 
@@ -323,23 +273,11 @@
  * @param str a pointer where the location of the result shall be stored
  * @param fmt the format string
  * @param ... additional arguments
- * @return the length of produced string or an error code from stdlib printf implementation
+ * @return the length of the produced string or an error code from stdlib printf implementation
  */
-cx_attr_nonnull_arg(1, 2, 4, 5)
-cx_attr_printf(5, 6)
-cx_attr_cstr_arg(5)
-cx_attr_access_rw(2)
-cx_attr_access_rw(3)
-cx_attr_access_rw(4)
-cx_attr_export
-int cx_sprintf_sa(
-        const CxAllocator *alloc,
-        char *buf,
-        size_t *len,
-        char **str,
-        const char *fmt,
-        ...
-);
+cx_attr_nonnull_arg(1, 2, 4, 5) cx_attr_printf(5, 6) cx_attr_cstr_arg(5)
+cx_attr_access_rw(2) cx_attr_access_rw(3) cx_attr_access_rw(4)
+CX_EXPORT int cx_sprintf_sa(const CxAllocator *alloc, char *buf, size_t *len, char **str, const char *fmt, ...);
 
 /**
  * An @c sprintf like function which allocates a new string when the buffer is not large enough.
@@ -359,7 +297,7 @@
  * @param str (@c char**) a pointer where the location of the result shall be stored
  * @param fmt (@c char*) the format string
  * @param ap (@c va_list) argument list
- * @return (@c int) the length of produced string or an error code from stdlib printf implementation
+ * @return (@c int) the length of the produced string or an error code from stdlib printf implementation
  */
 #define cx_vsprintf_s(buf, len, str, fmt, ap) cx_vsprintf_sa(cxDefaultAllocator, buf, len, str, fmt, ap)
 
@@ -382,19 +320,10 @@
  * @param str a pointer where the location of the result shall be stored
  * @param fmt the format string
  * @param ap argument list
- * @return the length of produced string or an error code from stdlib printf implementation
+ * @return the length of the produced string or an error code from stdlib printf implementation
  */
-cx_attr_nonnull
-cx_attr_cstr_arg(5)
-cx_attr_export
-int cx_vsprintf_sa(
-        const CxAllocator *alloc,
-        char *buf,
-        size_t *len,
-        char **str,
-        const char *fmt,
-        va_list ap
-);
+cx_attr_nonnull cx_attr_cstr_arg(5)
+CX_EXPORT int cx_vsprintf_sa(const CxAllocator *alloc, char *buf, size_t *len, char **str, const char *fmt, va_list ap);
 
 
 #ifdef __cplusplus
--- a/ucx/cx/properties.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/cx/properties.h	Sat Nov 08 23:06:11 2025 +0100
@@ -94,8 +94,7 @@
 /**
  * Default properties configuration.
  */
-cx_attr_export
-extern const CxPropertiesConfig cx_properties_config_default;
+CX_EXPORT extern const CxPropertiesConfig cx_properties_config_default;
 
 /**
  * Status codes for the properties interface.
@@ -122,7 +121,7 @@
      * You can use this enumerator to check for all "good" status results
      * by checking if the status is less than @c CX_PROPERTIES_OK.
      *
-     * A "good" status means, that you can refill data and continue parsing.
+     * A "good" status means that you can refill data and continue parsing.
      */
     CX_PROPERTIES_OK,
     /**
@@ -130,11 +129,11 @@
      */
     CX_PROPERTIES_NULL_INPUT,
     /**
-     * The line contains a delimiter, but no key.
+     * The line contains a delimiter but no key.
      */
     CX_PROPERTIES_INVALID_EMPTY_KEY,
     /**
-     * The line contains data, but no delimiter.
+     * The line contains data but no delimiter.
      */
     CX_PROPERTIES_INVALID_MISSING_DELIMITER,
     /**
@@ -200,7 +199,7 @@
 /**
  * A function that consumes a k/v-pair in a sink.
  *
- * The sink could be e.g. a map and the sink function would be calling
+ * The sink could be a map, and the sink function would be calling
  * a map function to store the k/v-pair.
  *
  * @param prop the properties interface that wants to sink a k/v-pair
@@ -210,7 +209,6 @@
  * @retval zero success
  * @retval non-zero sinking the k/v-pair failed
  */
-cx_attr_nonnull
 typedef int(*cx_properties_sink_func)(
         CxProperties *prop,
         CxPropertiesSink *sink,
@@ -257,7 +255,6 @@
  * @retval zero success
  * @retval non-zero reading the data failed
  */
-cx_attr_nonnull
 typedef int(*cx_properties_read_func)(
         CxProperties *prop,
         CxPropertiesSource *src,
@@ -272,7 +269,6 @@
  * @retval zero initialization was successful
  * @retval non-zero otherwise
  */
-cx_attr_nonnull
 typedef int(*cx_properties_read_init_func)(
         CxProperties *prop,
         CxPropertiesSource *src
@@ -284,7 +280,6 @@
  * @param prop the properties interface that wants to read from the source
  * @param src the source
  */
-cx_attr_nonnull
 typedef void(*cx_properties_read_clean_func)(
         CxProperties *prop,
         CxPropertiesSource *src
@@ -297,7 +292,7 @@
     /**
      * The source object.
      *
-     * For example a file stream or a string.
+     * For example, a file stream or a string.
      */
     void *src;
     /**
@@ -331,38 +326,32 @@
  * @see cxPropertiesInitDefault()
  */
 cx_attr_nonnull
-cx_attr_export
-void cxPropertiesInit(CxProperties *prop, CxPropertiesConfig config);
+CX_EXPORT void cxPropertiesInit(CxProperties *prop, CxPropertiesConfig config);
 
 /**
  * Destroys the properties interface.
  *
  * @note Even when you are certain that you did not use the interface in a
  * way that caused a memory allocation, you should call this function anyway.
- * Future versions of the library might add features that need additional memory
- * and you really don't want to search the entire code where you might need
- * add call to this function.
+ * Future versions of the library might add features that need additional memory,
+ * and you really don't want to search the entire code where you might need to
+ * add a call to this function.
  *
  * @param prop the properties interface
  */
 cx_attr_nonnull
-cx_attr_export
-void cxPropertiesDestroy(CxProperties *prop);
+CX_EXPORT void cxPropertiesDestroy(CxProperties *prop);
 
 /**
  * Destroys and re-initializes the properties interface.
  *
- * You might want to use this, to reset the parser after
+ * You might want to use this to reset the parser after
  * encountering a syntax error.
  *
  * @param prop the properties interface
  */
 cx_attr_nonnull
-static inline void cxPropertiesReset(CxProperties *prop) {
-    CxPropertiesConfig config = prop->config;
-    cxPropertiesDestroy(prop);
-    cxPropertiesInit(prop, config);
-}
+CX_EXPORT void cxPropertiesReset(CxProperties *prop);
 
 /**
  * Initialize a properties parser with the default configuration.
@@ -371,7 +360,7 @@
  * @see cxPropertiesInit()
  */
 #define cxPropertiesInitDefault(prop) \
-    cxPropertiesInit(prop, cx_properties_config_default)
+        cxPropertiesInit(prop, cx_properties_config_default)
 
 /**
  * Fills the input buffer with data.
@@ -394,44 +383,22 @@
  * @retval non-zero a memory allocation was necessary but failed
  * @see cxPropertiesFill()
  */
+cx_attr_nonnull cx_attr_access_r(2, 3)
+CX_EXPORT int cxPropertiesFilln(CxProperties *prop, const char *buf, size_t len);
+
+/**
+ * Internal function, do not use.
+ *
+ * @param prop the properties interface
+ * @param str the text to fill in
+ * @retval zero success
+ * @retval non-zero a memory allocation was necessary but failed
+ */
 cx_attr_nonnull
-cx_attr_access_r(2, 3)
-cx_attr_export
-int cxPropertiesFilln(
-        CxProperties *prop,
-        const char *buf,
-        size_t len
-);
-
-#ifdef __cplusplus
-} // extern "C"
-cx_attr_nonnull
-static inline int cxPropertiesFill(
-        CxProperties *prop,
-        cxstring str
-) {
+CX_INLINE int cx_properties_fill(CxProperties *prop, cxstring str) {
     return cxPropertiesFilln(prop, str.ptr, str.length);
 }
 
-cx_attr_nonnull
-static inline int cxPropertiesFill(
-        CxProperties *prop,
-        cxmutstr str
-) {
-    return cxPropertiesFilln(prop, str.ptr, str.length);
-}
-
-cx_attr_nonnull
-cx_attr_cstr_arg(2)
-static inline int cxPropertiesFill(
-        CxProperties *prop,
-        const char *str
-) {
-    return cxPropertiesFilln(prop, str, strlen(str));
-}
-
-extern "C" {
-#else // __cplusplus
 /**
  * Fills the input buffer with data.
  *
@@ -452,62 +419,17 @@
  * @retval non-zero a memory allocation was necessary but failed
  * @see cxPropertiesFilln()
  */
-#define cxPropertiesFill(prop, str) _Generic((str), \
-    cxstring: cx_properties_fill_cxstr,             \
-    cxmutstr: cx_properties_fill_mutstr,            \
-    char*: cx_properties_fill_str,                  \
-    const char*: cx_properties_fill_str)            \
-    (prop, str)
-
-/**
- * @copydoc cxPropertiesFill()
- */
-cx_attr_nonnull
-static inline int cx_properties_fill_cxstr(
-        CxProperties *prop,
-        cxstring str
-) {
-    return cxPropertiesFilln(prop, str.ptr, str.length);
-}
+#define cxPropertiesFill(prop, str) cx_properties_fill(prop, cx_strcast(str))
 
 /**
- * @copydoc cxPropertiesFill()
- */
-cx_attr_nonnull
-static inline int cx_properties_fill_mutstr(
-        CxProperties *prop,
-        cxmutstr str
-) {
-    return cxPropertiesFilln(prop, str.ptr, str.length);
-}
-
-/**
- * @copydoc cxPropertiesFill()
- */
-cx_attr_nonnull
-cx_attr_cstr_arg(2)
-static inline int cx_properties_fill_str(
-        CxProperties *prop,
-        const char *str
-) {
-    return cxPropertiesFilln(prop, str, strlen(str));
-}
-#endif
-
-/**
- * Specifies stack memory that shall be used as internal buffer.
+ * Specifies stack memory that shall be used as an internal buffer.
  *
  * @param prop the properties interface
  * @param buf a pointer to stack memory
  * @param capacity the capacity of the stack memory
  */
 cx_attr_nonnull
-cx_attr_export
-void cxPropertiesUseStack(
-        CxProperties *prop,
-        char *buf,
-        size_t capacity
-);
+CX_EXPORT void cxPropertiesUseStack(CxProperties *prop, char *buf, size_t capacity);
 
 /**
  * Retrieves the next key/value-pair.
@@ -539,14 +461,8 @@
  * @retval CX_PROPERTIES_INVALID_MISSING_DELIMITER the properties data contains a line without delimiter
  * @retval CX_PROPERTIES_BUFFER_ALLOC_FAILED an internal allocation was necessary but failed
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-CxPropertiesStatus cxPropertiesNext(
-        CxProperties *prop,
-        cxstring *key,
-        cxstring *value
-);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT CxPropertiesStatus cxPropertiesNext(CxProperties *prop, cxstring *key, cxstring *value);
 
 /**
  * Creates a properties sink for an UCX map.
@@ -562,10 +478,8 @@
  * @return the sink
  * @see cxPropertiesLoad()
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-CxPropertiesSink cxPropertiesMapSink(CxMap *map);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT CxPropertiesSink cxPropertiesMapSink(CxMap *map);
 
 /**
  * Creates a properties source based on an UCX string.
@@ -575,8 +489,7 @@
  * @see cxPropertiesLoad()
  */
 cx_attr_nodiscard
-cx_attr_export
-CxPropertiesSource cxPropertiesStringSource(cxstring str);
+CX_EXPORT CxPropertiesSource cxPropertiesStringSource(cxstring str);
 
 /**
  * Creates a properties source based on C string with the specified length.
@@ -586,11 +499,8 @@
  * @return the properties source
  * @see cxPropertiesLoad()
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_access_r(1, 2)
-cx_attr_export
-CxPropertiesSource cxPropertiesCstrnSource(const char *str, size_t len);
+cx_attr_nonnull cx_attr_nodiscard cx_attr_access_r(1, 2)
+CX_EXPORT CxPropertiesSource cxPropertiesCstrnSource(const char *str, size_t len);
 
 /**
  * Creates a properties source based on a C string.
@@ -602,11 +512,8 @@
  * @return the properties source
  * @see cxPropertiesLoad()
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_cstr_arg(1)
-cx_attr_export
-CxPropertiesSource cxPropertiesCstrSource(const char *str);
+cx_attr_nonnull cx_attr_nodiscard cx_attr_cstr_arg(1)
+CX_EXPORT CxPropertiesSource cxPropertiesCstrSource(const char *str);
 
 /**
  * Creates a properties source based on an FILE.
@@ -617,11 +524,8 @@
  * @return the properties source
  * @see cxPropertiesLoad()
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_access_r(1)
-cx_attr_export
-CxPropertiesSource cxPropertiesFileSource(FILE *file, size_t chunk_size);
+cx_attr_nonnull cx_attr_nodiscard cx_attr_access_r(1)
+CX_EXPORT CxPropertiesSource cxPropertiesFileSource(FILE *file, size_t chunk_size);
 
 
 /**
@@ -653,12 +557,8 @@
  * @retval CX_PROPERTIES_BUFFER_ALLOC_FAILED an internal allocation was necessary but failed
  */
 cx_attr_nonnull
-cx_attr_export
-CxPropertiesStatus cxPropertiesLoad(
-        CxProperties *prop,
-        CxPropertiesSink sink,
-        CxPropertiesSource source
-);
+CX_EXPORT CxPropertiesStatus cxPropertiesLoad(CxProperties *prop,
+        CxPropertiesSink sink, CxPropertiesSource source);
 
 #ifdef __cplusplus
 } // extern "C"
--- a/ucx/cx/streams.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/cx/streams.h	Sat Nov 08 23:06:11 2025 +0100
@@ -54,7 +54,7 @@
  * @param wfnc the write function
  * @param buf a pointer to the copy buffer or @c NULL if a buffer
  * shall be implicitly created on the heap
- * @param bufsize the size of the copy buffer - if @p buf is @c NULL you can
+ * @param bufsize the size of the copy buffer - if @p buf is @c NULL, you can
  * set this to zero to let the implementation decide
  * @param n the maximum number of bytes that shall be copied.
  * If this is larger than @p bufsize, the content is copied over multiple
@@ -62,19 +62,10 @@
  * @return the total number of bytes copied
  */
 cx_attr_nonnull_arg(1, 2, 3, 4)
-cx_attr_access_r(1)
-cx_attr_access_w(2)
-cx_attr_access_w(5)
-cx_attr_export
-size_t cx_stream_bncopy(
-        void *src,
-        void *dest,
-        cx_read_func rfnc,
-        cx_write_func wfnc,
-        char *buf,
-        size_t bufsize,
-        size_t n
-);
+cx_attr_access_r(1) cx_attr_access_w(2) cx_attr_access_w(5)
+CX_EXPORT size_t cx_stream_bncopy(void *src, void *dest,
+        cx_read_func rfnc, cx_write_func wfnc,
+        char *buf, size_t bufsize, size_t n);
 
 /**
  * Reads data from a stream and writes it to another stream.
@@ -86,7 +77,7 @@
  * @param buf (@c char*) a pointer to the copy buffer or @c NULL if a buffer
  * shall be implicitly created on the heap
  * @param bufsize (@c size_t) the size of the copy buffer - if @p buf is
- * @c NULL you can set this to zero to let the implementation decide
+ * @c NULL, you can set this to zero to let the implementation decide
  * @return total number of bytes copied
  */
 #define cx_stream_bcopy(src, dest, rfnc, wfnc, buf, bufsize) \
@@ -95,7 +86,7 @@
 /**
  * Reads data from a stream and writes it to another stream.
  *
- * The data is temporarily stored in a stack allocated buffer.
+ * The data is temporarily stored in a stack-allocated buffer.
  *
  * @param src the source stream
  * @param dest the destination stream
@@ -104,22 +95,14 @@
  * @param n the maximum number of bytes that shall be copied.
  * @return total number of bytes copied
  */
-cx_attr_nonnull
-cx_attr_access_r(1)
-cx_attr_access_w(2)
-cx_attr_export
-size_t cx_stream_ncopy(
-        void *src,
-        void *dest,
-        cx_read_func rfnc,
-        cx_write_func wfnc,
-        size_t n
-);
+cx_attr_nonnull cx_attr_access_r(1) cx_attr_access_w(2)
+CX_EXPORT size_t cx_stream_ncopy(void *src, void *dest,
+        cx_read_func rfnc, cx_write_func wfnc, size_t n);
 
 /**
  * Reads data from a stream and writes it to another stream.
  *
- * The data is temporarily stored in a stack allocated buffer.
+ * The data is temporarily stored in a stack-allocated buffer.
  *
  * @param src (@c void*) the source stream
  * @param dest (@c void*) the destination stream
--- a/ucx/cx/string.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/cx/string.h	Sat Nov 08 23:06:11 2025 +0100
@@ -48,8 +48,7 @@
 /**
  * The maximum length of the "needle" in cx_strstr() that can use SBO.
  */
-cx_attr_export
-extern const unsigned cx_strstr_sbo_size;
+CX_EXPORT extern const unsigned cx_strstr_sbo_size;
 
 /**
  * The UCX string structure.
@@ -112,10 +111,10 @@
      */
     size_t pos;
     /**
-     * Position of next delimiter in the source string.
+     * Position of the next delimiter in the source string.
      *
      * If the tokenizer has not yet returned a token, the content of this field
-     * is undefined. If the tokenizer reached the end of the string, this field
+     * is undefined. If the tokenizer reaches the end of the string, this field
      * contains the length of the source string.
      */
     size_t delim_pos;
@@ -174,15 +173,13 @@
  *
  * If you need to wrap a constant string, use cx_str().
  *
- * @param cstring the string to wrap, must be zero-terminated
+ * @param cstring the string to wrap (must be zero-terminated)
  * @return the wrapped string
  *
  * @see cx_mutstrn()
  */
-cx_attr_nodiscard
-cx_attr_cstr_arg(1)
-cx_attr_export
-cxmutstr cx_mutstr(char *cstring);
+cx_attr_nodiscard cx_attr_cstr_arg(1)
+CX_EXPORT cxmutstr cx_mutstr(char *cstring);
 
 /**
  * Wraps a string that does not need to be zero-terminated.
@@ -200,13 +197,8 @@
  *
  * @see cx_mutstr()
  */
-cx_attr_nodiscard
-cx_attr_access_rw(1, 2)
-cx_attr_export
-cxmutstr cx_mutstrn(
-        char *cstring,
-        size_t length
-);
+cx_attr_nodiscard cx_attr_access_rw(1, 2)
+CX_EXPORT cxmutstr cx_mutstrn(char *cstring, size_t length);
 
 /**
  * Wraps a string that must be zero-terminated.
@@ -220,15 +212,13 @@
  *
  * If you need to wrap a non-constant string, use cx_mutstr().
  *
- * @param cstring the string to wrap, must be zero-terminated
+ * @param cstring the string to wrap (must be zero-terminated)
  * @return the wrapped string
  *
  * @see cx_strn()
  */
-cx_attr_nodiscard
-cx_attr_cstr_arg(1)
-cx_attr_export
-cxstring cx_str(const char *cstring);
+cx_attr_nodiscard cx_attr_cstr_arg(1)
+CX_EXPORT cxstring cx_str(const char *cstring);
 
 
 /**
@@ -247,28 +237,27 @@
  *
  * @see cx_str()
  */
-cx_attr_nodiscard
-cx_attr_access_r(1, 2)
-cx_attr_export
-cxstring cx_strn(
-        const char *cstring,
-        size_t length
-);
+cx_attr_nodiscard cx_attr_access_r(1, 2)
+CX_EXPORT cxstring cx_strn(const char *cstring, size_t length);
 
 #ifdef __cplusplus
 } // extern "C"
 cx_attr_nodiscard
-static inline cxstring cx_strcast(cxmutstr str) {
+CX_CPPDECL cxstring cx_strcast(cxmutstr str) {
     return cx_strn(str.ptr, str.length);
 }
 cx_attr_nodiscard
-static inline cxstring cx_strcast(cxstring str) {
+CX_CPPDECL cxstring cx_strcast(cxstring str) {
     return str;
 }
 cx_attr_nodiscard
-static inline cxstring cx_strcast(const char *str) {
+CX_CPPDECL cxstring cx_strcast(const char *str) {
     return cx_str(str);
 }
+cx_attr_nodiscard
+CX_CPPDECL cxstring cx_strcast(const unsigned char *str) {
+    return cx_str(static_cast<const char*>(str));
+}
 extern "C" {
 #else
 /**
@@ -278,7 +267,7 @@
  * @see cx_strcast()
  */
 cx_attr_nodiscard
-static inline cxstring cx_strcast_m(cxmutstr str) {
+CX_INLINE cxstring cx_strcast_m(cxmutstr str) {
     return (cxstring) {str.ptr, str.length};
 }
 /**
@@ -288,7 +277,7 @@
  * @see cx_strcast()
  */
 cx_attr_nodiscard
-static inline cxstring cx_strcast_c(cxstring str) {
+CX_INLINE cxstring cx_strcast_c(cxstring str) {
     return str;
 }
 
@@ -299,25 +288,32 @@
  * @see cx_strcast()
  */
 cx_attr_nodiscard
-static inline cxstring cx_strcast_z(const char *str) {
+CX_INLINE cxstring cx_strcast_u(const unsigned char *str) {
+    return cx_str((const char*)str);
+}
+
+/**
+ * Internal function, do not use.
+ * @param str
+ * @return
+ * @see cx_strcast()
+ */
+cx_attr_nodiscard
+CX_INLINE cxstring cx_strcast_z(const char *str) {
     return cx_str(str);
 }
 
 /**
-* Casts a mutable string to an immutable string.
-*
-* Does nothing for already immutable strings.
-*
-* @note This is not seriously a cast. Instead, you get a copy
-* of the struct with the desired pointer type. Both structs still
-* point to the same location, though!
-*
-* @param str (@c cxstring or @c cxmutstr) the string to cast
-* @return (@c cxstring) an immutable copy of the string pointer
-*/
+ * Wraps any string into an UCX string.
+ *
+ * @param str (any supported string type) the string to cast
+ * @return (@c cxstring) the string wrapped as UCX string
+ */
 #define cx_strcast(str) _Generic((str), \
         cxmutstr: cx_strcast_m, \
         cxstring: cx_strcast_c, \
+        const unsigned char*: cx_strcast_u, \
+        unsigned char *: cx_strcast_u, \
         const char*: cx_strcast_z, \
         char *: cx_strcast_z) (str)
 #endif
@@ -325,37 +321,32 @@
 /**
  * Passes the pointer in this string to the cxDefaultAllocator's @c free() function.
  *
- * The pointer in the struct is set to @c NULL and the length is set to zero
+ * The pointer in the struct is set to @c NULL, and the length is set to zero,
  * which means that this function protects you against double-free.
  *
  * @note There is no implementation for cxstring, because it is unlikely that
  * you ever have a <code>const char*</code> you are really supposed to free.
- * If you encounter such situation, you should double-check your code.
+ * If you encounter such a situation, you should double-check your code.
  *
  * @param str the string to free
  */
-cx_attr_export
-void cx_strfree(cxmutstr *str);
+CX_EXPORT void cx_strfree(cxmutstr *str);
 
 /**
- * Passes the pointer in this string to the allocators free function.
+ * Passes the pointer in this string to the allocator's free function.
  *
- * The pointer in the struct is set to @c NULL and the length is set to zero
+ * The pointer in the struct is set to @c NULL, and the length is set to zero,
  * which means that this function protects you against double-free.
  *
  * @note There is no implementation for cxstring, because it is unlikely that
  * you ever have a <code>const char*</code> you are really supposed to free.
- * If you encounter such situation, you should double-check your code.
+ * If you encounter such a situation, you should double-check your code.
  *
  * @param alloc the allocator
  * @param str the string to free
  */
 cx_attr_nonnull_arg(1)
-cx_attr_export
-void cx_strfree_a(
-        const CxAllocator *alloc,
-        cxmutstr *str
-);
+CX_EXPORT void cx_strfree_a(const CxAllocator *alloc, cxmutstr *str);
 
 /**
  * Copies a string.
@@ -373,12 +364,7 @@
  * @retval non-zero if re-allocation failed
  */
 cx_attr_nonnull_arg(1)
-cx_attr_export
-int cx_strcpy_a(
-        const CxAllocator *alloc,
-        cxmutstr *dest,
-        cxstring src
-);
+CX_EXPORT int cx_strcpy_a(const CxAllocator *alloc, cxmutstr *dest, cxstring src);
 
 
 /**
@@ -410,11 +396,7 @@
  * @return the accumulated length of all strings
  */
 cx_attr_nodiscard
-cx_attr_export
-size_t cx_strlen(
-        size_t count,
-        ...
-);
+CX_EXPORT size_t cx_strlen(size_t count, ...);
 
 /**
  * Concatenates strings.
@@ -438,15 +420,9 @@
  * @param ...   all other UCX strings
  * @return the concatenated string
  */
-cx_attr_nodiscard
-cx_attr_nonnull
-cx_attr_export
-cxmutstr cx_strcat_ma(
-        const CxAllocator *alloc,
-        cxmutstr str,
-        size_t count,
-        ...
-);
+cx_attr_nodiscard cx_attr_nonnull
+CX_EXPORT cxmutstr cx_strcat_ma(const CxAllocator *alloc,
+        cxmutstr str, size_t count, ...);
 
 /**
  * Concatenates strings and returns a new string.
@@ -467,7 +443,7 @@
  * @return (@c cxmutstr) the concatenated string
  */
 #define cx_strcat_a(alloc, count, ...) \
-cx_strcat_ma(alloc, cx_mutstrn(NULL, 0), count, __VA_ARGS__)
+        cx_strcat_ma(alloc, cx_mutstrn(NULL, 0), count, __VA_ARGS__)
 
 /**
  * Concatenates strings and returns a new string.
@@ -487,7 +463,7 @@
  * @return (@c cxmutstr) the concatenated string
  */
 #define cx_strcat(count, ...) \
-cx_strcat_ma(cxDefaultAllocator, cx_mutstrn(NULL, 0), count, __VA_ARGS__)
+        cx_strcat_ma(cxDefaultAllocator, cx_mutstrn(NULL, 0), count, __VA_ARGS__)
 
 /**
  * Concatenates strings.
@@ -511,7 +487,7 @@
  * @return (@c cxmutstr) the concatenated string
  */
 #define cx_strcat_m(str, count, ...) \
-cx_strcat_ma(cxDefaultAllocator, str, count, __VA_ARGS__)
+        cx_strcat_ma(cxDefaultAllocator, str, count, __VA_ARGS__)
 
 /**
  * Returns a substring starting at the specified location.
@@ -529,11 +505,7 @@
  * @see cx_strsubsl_m()
  */
 cx_attr_nodiscard
-cx_attr_export
-cxstring cx_strsubs(
-        cxstring string,
-        size_t start
-);
+CX_EXPORT cxstring cx_strsubs(cxstring string, size_t start);
 
 /**
  * Returns a substring starting at the specified location.
@@ -555,12 +527,7 @@
  * @see cx_strsubsl_m()
  */
 cx_attr_nodiscard
-cx_attr_export
-cxstring cx_strsubsl(
-        cxstring string,
-        size_t start,
-        size_t length
-);
+CX_EXPORT cxstring cx_strsubsl(cxstring string, size_t start, size_t length);
 
 /**
  * Returns a substring starting at the specified location.
@@ -578,11 +545,7 @@
  * @see cx_strsubsl()
  */
 cx_attr_nodiscard
-cx_attr_export
-cxmutstr cx_strsubs_m(
-        cxmutstr string,
-        size_t start
-);
+CX_EXPORT cxmutstr cx_strsubs_m(cxmutstr string, size_t start);
 
 /**
  * Returns a substring starting at the specified location.
@@ -604,12 +567,7 @@
  * @see cx_strsubsl()
  */
 cx_attr_nodiscard
-cx_attr_export
-cxmutstr cx_strsubsl_m(
-        cxmutstr string,
-        size_t start,
-        size_t length
-);
+CX_EXPORT cxmutstr cx_strsubsl_m(cxmutstr string, size_t start, size_t length);
 
 /**
  * Returns a substring starting at the location of the first occurrence of the
@@ -624,11 +582,7 @@
  * @see cx_strchr_m()
  */
 cx_attr_nodiscard
-cx_attr_export
-cxstring cx_strchr(
-        cxstring string,
-        int chr
-);
+CX_EXPORT cxstring cx_strchr(cxstring string, int chr);
 
 /**
  * Returns a substring starting at the location of the first occurrence of the
@@ -643,11 +597,7 @@
  * @see cx_strchr()
  */
 cx_attr_nodiscard
-cx_attr_export
-cxmutstr cx_strchr_m(
-        cxmutstr string,
-        int chr
-);
+CX_EXPORT cxmutstr cx_strchr_m(cxmutstr string, int chr);
 
 /**
  * Returns a substring starting at the location of the last occurrence of the
@@ -662,11 +612,7 @@
  * @see cx_strrchr_m()
  */
 cx_attr_nodiscard
-cx_attr_export
-cxstring cx_strrchr(
-        cxstring string,
-        int chr
-);
+CX_EXPORT cxstring cx_strrchr(cxstring string, int chr);
 
 /**
  * Returns a substring starting at the location of the last occurrence of the
@@ -681,11 +627,7 @@
  * @see cx_strrchr()
  */
 cx_attr_nodiscard
-cx_attr_export
-cxmutstr cx_strrchr_m(
-        cxmutstr string,
-        int chr
-);
+CX_EXPORT cxmutstr cx_strrchr_m(cxmutstr string, int chr);
 
 /**
  * Returns a substring starting at the location of the first occurrence of the
@@ -704,11 +646,7 @@
  * @see cx_strstr_m()
  */
 cx_attr_nodiscard
-cx_attr_export
-cxstring cx_strstr(
-        cxstring haystack,
-        cxstring needle
-);
+CX_EXPORT cxstring cx_strstr(cxstring haystack, cxstring needle);
 
 /**
  * Returns a substring starting at the location of the first occurrence of the
@@ -727,11 +665,7 @@
  * @see cx_strstr()
  */
 cx_attr_nodiscard
-cx_attr_export
-cxmutstr cx_strstr_m(
-        cxmutstr haystack,
-        cxstring needle
-);
+CX_EXPORT cxmutstr cx_strstr_m(cxmutstr haystack, cxstring needle);
 
 /**
  * Splits a given string using a delimiter string.
@@ -745,16 +679,9 @@
  * @param output a preallocated array of at least @p limit length
  * @return the actual number of split items
  */
-cx_attr_nodiscard
-cx_attr_nonnull
-cx_attr_access_w(4, 3)
-cx_attr_export
-size_t cx_strsplit(
-        cxstring string,
-        cxstring delim,
-        size_t limit,
-        cxstring *output
-);
+cx_attr_nodiscard cx_attr_nonnull cx_attr_access_w(4, 3)
+CX_EXPORT size_t cx_strsplit(cxstring string, cxstring delim,
+        size_t limit, cxstring *output);
 
 /**
  * Splits a given string using a delimiter string.
@@ -775,17 +702,10 @@
  * written to
  * @return the actual number of split items
  */
-cx_attr_nodiscard
-cx_attr_nonnull
-cx_attr_access_w(5)
-cx_attr_export
-size_t cx_strsplit_a(
-        const CxAllocator *allocator,
-        cxstring string,
-        cxstring delim,
-        size_t limit,
-        cxstring **output
-);
+cx_attr_nodiscard cx_attr_nonnull cx_attr_access_w(5)
+CX_EXPORT size_t cx_strsplit_a(const CxAllocator *allocator,
+        cxstring string, cxstring delim,
+        size_t limit, cxstring **output);
 
 
 /**
@@ -800,16 +720,9 @@
  * @param output a preallocated array of at least @p limit length
  * @return the actual number of split items
  */
-cx_attr_nodiscard
-cx_attr_nonnull
-cx_attr_access_w(4, 3)
-cx_attr_export
-size_t cx_strsplit_m(
-        cxmutstr string,
-        cxstring delim,
-        size_t limit,
-        cxmutstr *output
-);
+cx_attr_nodiscard cx_attr_nonnull cx_attr_access_w(4, 3)
+CX_EXPORT size_t cx_strsplit_m(cxmutstr string, cxstring delim,
+        size_t limit, cxmutstr *output);
 
 /**
  * Splits a given string using a delimiter string.
@@ -830,17 +743,10 @@
  * written to
  * @return the actual number of split items
  */
-cx_attr_nodiscard
-cx_attr_nonnull
-cx_attr_access_w(5)
-cx_attr_export
-size_t cx_strsplit_ma(
-        const CxAllocator *allocator,
-        cxmutstr string,
-        cxstring delim,
-        size_t limit,
-        cxmutstr **output
-);
+cx_attr_nodiscard cx_attr_nonnull cx_attr_access_w(5)
+CX_EXPORT size_t cx_strsplit_ma(const CxAllocator *allocator,
+        cxmutstr string, cxstring delim, size_t limit,
+        cxmutstr **output);
 
 /**
  * Compares two strings.
@@ -851,11 +757,17 @@
  * than @p s2, zero if both strings equal
  */
 cx_attr_nodiscard
-cx_attr_export
-int cx_strcmp(
-        cxstring s1,
-        cxstring s2
-);
+CX_EXPORT int cx_strcmp_(cxstring s1, cxstring s2);
+
+/**
+ * Compares two strings.
+ *
+ * @param s1 the first string
+ * @param s2 the second string
+ * @return negative if @p s1 is smaller than @p s2, positive if @p s1 is larger
+ * than @p s2, zero if both strings equal
+ */
+#define cx_strcmp(s1, s2) cx_strcmp_(cx_strcast(s1), cx_strcast(s2))
 
 /**
  * Compares two strings ignoring case.
@@ -866,29 +778,33 @@
  * than @p s2, zero if both strings equal ignoring case
  */
 cx_attr_nodiscard
-cx_attr_export
-int cx_strcasecmp(
-        cxstring s1,
-        cxstring s2
-);
+CX_EXPORT int cx_strcasecmp_(cxstring s1, cxstring s2);
+
+/**
+ * Compares two strings ignoring case.
+ *
+ * @param s1 the first string
+ * @param s2 the second string
+ * @return negative if @p s1 is smaller than @p s2, positive if @p s1 is larger
+ * than @p s2, zero if both strings equal ignoring case
+ */
+#define cx_strcasecmp(s1, s2) cx_strcasecmp_(cx_strcast(s1), cx_strcast(s2))
 
 /**
  * Compares two strings.
  *
  * This function has a compatible signature for the use as a cx_compare_func.
  *
+ * @attention This function can @em only compare UCX strings. It is unsafe to
+ * pass normal C-strings to this function.
+ *
  * @param s1 the first string
  * @param s2 the second string
  * @return negative if @p s1 is smaller than @p s2, positive if @p s1 is larger
  * than @p s2, zero if both strings equal
  */
-cx_attr_nodiscard
-cx_attr_nonnull
-cx_attr_export
-int cx_strcmp_p(
-        const void *s1,
-        const void *s2
-);
+cx_attr_nodiscard  cx_attr_nonnull
+CX_EXPORT int cx_strcmp_p(const void *s1, const void *s2);
 
 /**
  * Compares two strings ignoring case.
@@ -900,13 +816,8 @@
  * @return negative if @p s1 is smaller than @p s2, positive if @p s1 is larger
  * than @p s2, zero if both strings equal ignoring case
  */
-cx_attr_nodiscard
-cx_attr_nonnull
-cx_attr_export
-int cx_strcasecmp_p(
-        const void *s1,
-        const void *s2
-);
+cx_attr_nodiscard cx_attr_nonnull
+CX_EXPORT int cx_strcasecmp_p(const void *s1, const void *s2);
 
 
 /**
@@ -921,13 +832,8 @@
  * @return a duplicate of the string
  * @see cx_strdup()
  */
-cx_attr_nodiscard
-cx_attr_nonnull
-cx_attr_export
-cxmutstr cx_strdup_a_(
-        const CxAllocator *allocator,
-        cxstring string
-);
+cx_attr_nodiscard cx_attr_nonnull
+CX_EXPORT cxmutstr cx_strdup_a_(const CxAllocator *allocator, cxstring string);
 
 /**
  * Creates a duplicate of the specified string.
@@ -942,8 +848,7 @@
  * @see cx_strdup()
  * @see cx_strfree_a()
  */
-#define cx_strdup_a(allocator, string) \
-    cx_strdup_a_((allocator), cx_strcast(string))
+#define cx_strdup_a(allocator, string) cx_strdup_a_((allocator), cx_strcast(string))
 
 /**
  * Creates a duplicate of the specified string.
@@ -970,8 +875,7 @@
  * @return the trimmed string
  */
 cx_attr_nodiscard
-cx_attr_export
-cxstring cx_strtrim(cxstring string);
+CX_EXPORT cxstring cx_strtrim(cxstring string);
 
 /**
  * Omits leading and trailing spaces.
@@ -983,11 +887,10 @@
  * @return the trimmed string
  */
 cx_attr_nodiscard
-cx_attr_export
-cxmutstr cx_strtrim_m(cxmutstr string);
+CX_EXPORT cxmutstr cx_strtrim_m(cxmutstr string);
 
 /**
- * Checks, if a string has a specific prefix.
+ * Checks if a string has a specific prefix.
  *
  * @param string the string to check
  * @param prefix the prefix the string should have
@@ -995,14 +898,20 @@
  * @c false otherwise
  */
 cx_attr_nodiscard
-cx_attr_export
-bool cx_strprefix(
-        cxstring string,
-        cxstring prefix
-);
+CX_EXPORT bool cx_strprefix_(cxstring string, cxstring prefix);
 
 /**
- * Checks, if a string has a specific suffix.
+ * Checks if a string has a specific prefix.
+ *
+ * @param string the string to check
+ * @param prefix the prefix the string should have
+ * @return @c true, if and only if the string has the specified prefix,
+ * @c false otherwise
+ */
+#define cx_strprefix(string, prefix) cx_strprefix_(cx_strcast(string), cx_strcast(prefix))
+
+/**
+ * Checks if a string has a specific suffix.
  *
  * @param string the string to check
  * @param suffix the suffix the string should have
@@ -1010,14 +919,20 @@
  * @c false otherwise
  */
 cx_attr_nodiscard
-cx_attr_export
-bool cx_strsuffix(
-        cxstring string,
-        cxstring suffix
-);
+CX_EXPORT bool cx_strsuffix_(cxstring string, cxstring suffix);
 
 /**
- * Checks, if a string has a specific prefix, ignoring the case.
+ * Checks if a string has a specific suffix.
+ *
+ * @param string the string to check
+ * @param suffix the suffix the string should have
+ * @return @c true, if and only if the string has the specified suffix,
+ * @c false otherwise
+ */
+#define cx_strsuffix(string, suffix) cx_strsuffix_(cx_strcast(string), cx_strcast(suffix))
+
+/**
+ * Checks if a string has a specific prefix, ignoring the case.
  *
  * @param string the string to check
  * @param prefix the prefix the string should have
@@ -1025,11 +940,17 @@
  * @c false otherwise
  */
 cx_attr_nodiscard
-cx_attr_export
-bool cx_strcaseprefix(
-        cxstring string,
-        cxstring prefix
-);
+CX_EXPORT bool cx_strcaseprefix_(cxstring string, cxstring prefix);
+
+/**
+ * Checks if a string has a specific prefix, ignoring the case.
+ *
+ * @param string the string to check
+ * @param prefix the prefix the string should have
+ * @return @c true, if and only if the string has the specified prefix,
+ * @c false otherwise
+ */
+#define cx_strcaseprefix(string, prefix) cx_strcaseprefix_(cx_strcast(string), cx_strcast(prefix))
 
 /**
  * Checks, if a string has a specific suffix, ignoring the case.
@@ -1040,16 +961,22 @@
  * @c false otherwise
  */
 cx_attr_nodiscard
-cx_attr_export
-bool cx_strcasesuffix(
-        cxstring string,
-        cxstring suffix
-);
+CX_EXPORT bool cx_strcasesuffix_(cxstring string, cxstring suffix);
+
+/**
+ * Checks, if a string has a specific suffix, ignoring the case.
+ *
+ * @param string the string to check
+ * @param suffix the suffix the string should have
+ * @return @c true, if and only if the string has the specified suffix,
+ * @c false otherwise
+ */
+#define cx_strcasesuffix(string, suffix) cx_strcasesuffix_(cx_strcast(string), cx_strcast(suffix))
 
 /**
  * Replaces a string with another string.
  *
- * Replaces at most @p replmax occurrences.
+ * The function replaces at most @p replmax occurrences.
  *
  * The returned string will be allocated by @p allocator and is guaranteed
  * to be zero-terminated.
@@ -1064,21 +991,14 @@
  * @param replmax maximum number of replacements
  * @return the resulting string after applying the replacements
  */
-cx_attr_nodiscard
-cx_attr_nonnull
-cx_attr_export
-cxmutstr cx_strreplacen_a(
-        const CxAllocator *allocator,
-        cxstring str,
-        cxstring search,
-        cxstring replacement,
-        size_t replmax
-);
+cx_attr_nodiscard cx_attr_nonnull
+CX_EXPORT cxmutstr cx_strreplacen_a(const CxAllocator *allocator,
+        cxstring str, cxstring search, cxstring replacement, size_t replmax);
 
 /**
  * Replaces a string with another string.
  *
- * Replaces at most @p replmax occurrences.
+ * The function replaces at most @p replmax occurrences.
  *
  * The returned string will be allocated by the cxDefaultAllocator and is guaranteed
  * to be zero-terminated.
@@ -1093,7 +1013,7 @@
  * @return (@c cxmutstr) the resulting string after applying the replacements
  */
 #define cx_strreplacen(str, search, replacement, replmax) \
-cx_strreplacen_a(cxDefaultAllocator, str, search, replacement, replmax)
+        cx_strreplacen_a(cxDefaultAllocator, str, search, replacement, replmax)
 
 /**
  * Replaces a string with another string.
@@ -1111,7 +1031,7 @@
  * @return (@c cxmutstr) the resulting string after applying the replacements
  */
 #define cx_strreplace_a(allocator, str, search, replacement) \
-cx_strreplacen_a(allocator, str, search, replacement, SIZE_MAX)
+        cx_strreplacen_a(allocator, str, search, replacement, SIZE_MAX)
 
 /**
  * Replaces a string with another string.
@@ -1128,7 +1048,7 @@
  * @return (@c cxmutstr) the resulting string after applying the replacements
  */
 #define cx_strreplace(str, search, replacement) \
-cx_strreplacen_a(cxDefaultAllocator, str, search, replacement, SIZE_MAX)
+        cx_strreplacen_a(cxDefaultAllocator, str, search, replacement, SIZE_MAX)
 
 /**
  * Creates a string tokenization context.
@@ -1139,12 +1059,7 @@
  * @return a new string tokenization context
  */
 cx_attr_nodiscard
-cx_attr_export
-CxStrtokCtx cx_strtok_(
-        cxstring str,
-        cxstring delim,
-        size_t limit
-);
+CX_EXPORT CxStrtokCtx cx_strtok_(cxstring str, cxstring delim, size_t limit);
 
 /**
  * Creates a string tokenization context.
@@ -1155,7 +1070,7 @@
  * @return (@c CxStrtokCtx) a new string tokenization context
  */
 #define cx_strtok(str, delim, limit) \
-    cx_strtok_(cx_strcast(str), cx_strcast(delim), (limit))
+        cx_strtok_(cx_strcast(str), cx_strcast(delim), (limit))
 
 /**
  * Returns the next token.
@@ -1167,14 +1082,8 @@
  * @return true if successful, false if the limit or the end of the string
  * has been reached
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_access_w(2)
-cx_attr_export
-bool cx_strtok_next(
-        CxStrtokCtx *ctx,
-        cxstring *token
-);
+cx_attr_nonnull  cx_attr_nodiscard  cx_attr_access_w(2)
+CX_EXPORT bool cx_strtok_next(CxStrtokCtx *ctx, cxstring *token);
 
 /**
  * Returns the next token of a mutable string.
@@ -1190,14 +1099,8 @@
  * @return true if successful, false if the limit or the end of the string
  * has been reached
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_access_w(2)
-cx_attr_export
-bool cx_strtok_next_m(
-        CxStrtokCtx *ctx,
-        cxmutstr *token
-);
+cx_attr_nonnull  cx_attr_nodiscard  cx_attr_access_w(2)
+CX_EXPORT bool cx_strtok_next_m(CxStrtokCtx *ctx, cxmutstr *token);
 
 /**
  * Defines an array of more delimiters for the specified tokenization context.
@@ -1206,14 +1109,8 @@
  * @param delim array of more delimiters
  * @param count number of elements in the array
  */
-cx_attr_nonnull
-cx_attr_access_r(2, 3)
-cx_attr_export
-void cx_strtok_delim(
-        CxStrtokCtx *ctx,
-        const cxstring *delim,
-        size_t count
-);
+cx_attr_nonnull cx_attr_access_r(2, 3)
+CX_EXPORT void cx_strtok_delim(CxStrtokCtx *ctx, const cxstring *delim, size_t count);
 
 /* ------------------------------------------------------------------------- *
  *                string to number conversion functions                      *
@@ -1229,12 +1126,12 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
-int cx_strtos_lc_(cxstring str, short *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+CX_EXPORT int cx_strtos_lc_(cxstring str, short *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1246,12 +1143,12 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
-int cx_strtoi_lc_(cxstring str, int *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+CX_EXPORT int cx_strtoi_lc_(cxstring str, int *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1263,12 +1160,12 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
-int cx_strtol_lc_(cxstring str, long *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+CX_EXPORT int cx_strtol_lc_(cxstring str, long *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1280,12 +1177,12 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
-int cx_strtoll_lc_(cxstring str, long long *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+CX_EXPORT int cx_strtoll_lc_(cxstring str, long long *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1297,12 +1194,12 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
-int cx_strtoi8_lc_(cxstring str, int8_t *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+CX_EXPORT int cx_strtoi8_lc_(cxstring str, int8_t *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1314,12 +1211,12 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
-int cx_strtoi16_lc_(cxstring str, int16_t *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+CX_EXPORT int cx_strtoi16_lc_(cxstring str, int16_t *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1331,12 +1228,12 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
-int cx_strtoi32_lc_(cxstring str, int32_t *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+CX_EXPORT int cx_strtoi32_lc_(cxstring str, int32_t *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1348,12 +1245,12 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
-int cx_strtoi64_lc_(cxstring str, int64_t *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+CX_EXPORT int cx_strtoi64_lc_(cxstring str, int64_t *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1365,12 +1262,12 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
-int cx_strtous_lc_(cxstring str, unsigned short *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+CX_EXPORT int cx_strtous_lc_(cxstring str, unsigned short *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1382,12 +1279,12 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
-int cx_strtou_lc_(cxstring str, unsigned int *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+CX_EXPORT int cx_strtou_lc_(cxstring str, unsigned int *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1399,12 +1296,12 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
-int cx_strtoul_lc_(cxstring str, unsigned long *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+CX_EXPORT int cx_strtoul_lc_(cxstring str, unsigned long *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1416,12 +1313,12 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
-int cx_strtoull_lc_(cxstring str, unsigned long long *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+CX_EXPORT int cx_strtoull_lc_(cxstring str, unsigned long long *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1433,12 +1330,12 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
-int cx_strtou8_lc_(cxstring str, uint8_t *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+CX_EXPORT int cx_strtou8_lc_(cxstring str, uint8_t *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1450,12 +1347,12 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
-int cx_strtou16_lc_(cxstring str, uint16_t *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+CX_EXPORT int cx_strtou16_lc_(cxstring str, uint16_t *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1467,12 +1364,12 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
-int cx_strtou32_lc_(cxstring str, uint32_t *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+CX_EXPORT int cx_strtou32_lc_(cxstring str, uint32_t *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1484,12 +1381,12 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
-int cx_strtou64_lc_(cxstring str, uint64_t *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+CX_EXPORT int cx_strtou64_lc_(cxstring str, uint64_t *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1501,15 +1398,15 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
-int cx_strtoz_lc_(cxstring str, size_t *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+CX_EXPORT int cx_strtoz_lc_(cxstring str, size_t *output, int base, const char *groupsep);
 
 /**
- * Converts a string to a single precision floating point number.
+ * Converts a string to a single precision floating-point number.
  *
  * The function returns non-zero when conversion is not possible.
  * In that case the function sets errno to EINVAL when the reason is an invalid character.
@@ -1518,15 +1415,15 @@
  * @param str the string to convert
  * @param output a pointer to the float variable where the result shall be stored
  * @param decsep the decimal separator
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
-int cx_strtof_lc_(cxstring str, float *output, char decsep, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+CX_EXPORT int cx_strtof_lc_(cxstring str, float *output, char decsep, const char *groupsep);
 
 /**
- * Converts a string to a double precision floating point number.
+ * Converts a string to a double precision floating-point number.
  *
  * The function returns non-zero when conversion is not possible.
  * In that case the function sets errno to EINVAL when the reason is an invalid character.
@@ -1535,12 +1432,12 @@
  * @param str the string to convert
  * @param output a pointer to the float variable where the result shall be stored
  * @param decsep the decimal separator
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
-int cx_strtod_lc_(cxstring str, double *output, char decsep, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+CX_EXPORT int cx_strtod_lc_(cxstring str, double *output, char decsep, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1552,7 +1449,7 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep (@c const @c char*) each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
@@ -1568,7 +1465,7 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep (@c const @c char*) each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
@@ -1584,7 +1481,7 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep (@c const @c char*) each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
@@ -1600,7 +1497,7 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep (@c const @c char*) each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
@@ -1616,7 +1513,7 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep (@c const @c char*) each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
@@ -1632,7 +1529,7 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep (@c const @c char*) each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
@@ -1648,7 +1545,7 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep (@c const @c char*) each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
@@ -1664,7 +1561,7 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep (@c const @c char*) each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
@@ -1680,7 +1577,7 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep (@c const @c char*) each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
@@ -1696,7 +1593,7 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep (@c const @c char*) each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
@@ -1712,7 +1609,7 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep (@c const @c char*) each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
@@ -1728,7 +1625,7 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep (@c const @c char*) each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
@@ -1744,7 +1641,7 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep (@c const @c char*) each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
@@ -1760,7 +1657,7 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep (@c const @c char*) each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
@@ -1776,7 +1673,7 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep (@c const @c char*) each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
@@ -1792,7 +1689,7 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep (@c const @c char*) each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
@@ -1808,7 +1705,7 @@
  * @param str the string to convert
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
- * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep (@c const @c char*) each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
@@ -1821,7 +1718,7 @@
  * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
  * It sets errno to ERANGE when the target datatype is too small.
  *
- * The comma character is treated as group separator and ignored during parsing.
+ * The comma character is treated as a group separator and ignored during parsing.
  * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
  *
  * @param str the string to convert
@@ -1839,7 +1736,7 @@
  * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
  * It sets errno to ERANGE when the target datatype is too small.
  *
- * The comma character is treated as group separator and ignored during parsing.
+ * The comma character is treated as a group separator and ignored during parsing.
  * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
  *
  * @param str the string to convert
@@ -1857,7 +1754,7 @@
  * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
  * It sets errno to ERANGE when the target datatype is too small.
  *
- * The comma character is treated as group separator and ignored during parsing.
+ * The comma character is treated as a group separator and ignored during parsing.
  * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
  *
  * @param str the string to convert
@@ -1875,7 +1772,7 @@
  * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
  * It sets errno to ERANGE when the target datatype is too small.
  *
- * The comma character is treated as group separator and ignored during parsing.
+ * The comma character is treated as a group separator and ignored during parsing.
  * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
  *
  * @param str the string to convert
@@ -1893,7 +1790,7 @@
  * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
  * It sets errno to ERANGE when the target datatype is too small.
  *
- * The comma character is treated as group separator and ignored during parsing.
+ * The comma character is treated as a group separator and ignored during parsing.
  * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
  *
  * @param str the string to convert
@@ -1911,7 +1808,7 @@
  * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
  * It sets errno to ERANGE when the target datatype is too small.
  *
- * The comma character is treated as group separator and ignored during parsing.
+ * The comma character is treated as a group separator and ignored during parsing.
  * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
  *
  * @param str the string to convert
@@ -1929,7 +1826,7 @@
  * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
  * It sets errno to ERANGE when the target datatype is too small.
  *
- * The comma character is treated as group separator and ignored during parsing.
+ * The comma character is treated as a group separator and ignored during parsing.
  * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
  *
  * @param str the string to convert
@@ -1947,7 +1844,7 @@
  * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
  * It sets errno to ERANGE when the target datatype is too small.
  *
- * The comma character is treated as group separator and ignored during parsing.
+ * The comma character is treated as a group separator and ignored during parsing.
  * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
  *
  * @param str the string to convert
@@ -1965,7 +1862,7 @@
  * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
  * It sets errno to ERANGE when the target datatype is too small.
  *
- * The comma character is treated as group separator and ignored during parsing.
+ * The comma character is treated as a group separator and ignored during parsing.
  * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
  *
  * @param str the string to convert
@@ -1983,7 +1880,7 @@
  * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
  * It sets errno to ERANGE when the target datatype is too small.
  *
- * The comma character is treated as group separator and ignored during parsing.
+ * The comma character is treated as a group separator and ignored during parsing.
  * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
  *
  * @param str the string to convert
@@ -2001,7 +1898,7 @@
  * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
  * It sets errno to ERANGE when the target datatype is too small.
  *
- * The comma character is treated as group separator and ignored during parsing.
+ * The comma character is treated as a group separator and ignored during parsing.
  * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
  *
  * @param str the string to convert
@@ -2019,7 +1916,7 @@
  * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
  * It sets errno to ERANGE when the target datatype is too small.
  *
- * The comma character is treated as group separator and ignored during parsing.
+ * The comma character is treated as a group separator and ignored during parsing.
  * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
  *
  * @param str the string to convert
@@ -2037,7 +1934,7 @@
  * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
  * It sets errno to ERANGE when the target datatype is too small.
  *
- * The comma character is treated as group separator and ignored during parsing.
+ * The comma character is treated as a group separator and ignored during parsing.
  * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
  *
  * @param str the string to convert
@@ -2055,7 +1952,7 @@
  * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
  * It sets errno to ERANGE when the target datatype is too small.
  *
- * The comma character is treated as group separator and ignored during parsing.
+ * The comma character is treated as a group separator and ignored during parsing.
  * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
  *
  * @param str the string to convert
@@ -2073,7 +1970,7 @@
  * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
  * It sets errno to ERANGE when the target datatype is too small.
  *
- * The comma character is treated as group separator and ignored during parsing.
+ * The comma character is treated as a group separator and ignored during parsing.
  * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
  *
  * @param str the string to convert
@@ -2091,7 +1988,7 @@
  * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
  * It sets errno to ERANGE when the target datatype is too small.
  *
- * The comma character is treated as group separator and ignored during parsing.
+ * The comma character is treated as a group separator and ignored during parsing.
  * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
  *
  * @param str the string to convert
@@ -2109,7 +2006,7 @@
  * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
  * It sets errno to ERANGE when the target datatype is too small.
  *
- * The comma character is treated as group separator and ignored during parsing.
+ * The comma character is treated as a group separator and ignored during parsing.
  * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
  *
  * @param str the string to convert
@@ -2121,7 +2018,7 @@
 #define cx_strtou64(str, output, base) cx_strtou64_lc_(cx_strcast(str), output, base, ",")
 
 /**
- * Converts a string to a single precision floating point number.
+ * Converts a string to a single precision floating-point number.
  *
  * The function returns non-zero when conversion is not possible.
  * In that case the function sets errno to EINVAL when the reason is an invalid character.
@@ -2130,14 +2027,14 @@
  * @param str the string to convert
  * @param output a pointer to the float variable where the result shall be stored
  * @param decsep the decimal separator
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
 #define cx_strtof_lc(str, output, decsep, groupsep) cx_strtof_lc_(cx_strcast(str), output, decsep, groupsep)
 
 /**
- * Converts a string to a double precision floating point number.
+ * Converts a string to a double precision floating-point number.
  *
  * The function returns non-zero when conversion is not possible.
  * In that case the function sets errno to EINVAL when the reason is an invalid character.
@@ -2145,21 +2042,21 @@
  * @param str the string to convert
  * @param output a pointer to the double variable where the result shall be stored
  * @param decsep the decimal separator
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @param groupsep each character in this string is treated as a group separator and ignored during conversion
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
 #define cx_strtod_lc(str, output, decsep, groupsep) cx_strtod_lc_(cx_strcast(str), output, decsep, groupsep)
 
 /**
- * Converts a string to a single precision floating point number.
+ * Converts a string to a single precision floating-point number.
  *
  * The function returns non-zero when conversion is not possible.
  * In that case the function sets errno to EINVAL when the reason is an invalid character.
  * It sets errno to ERANGE when the necessary representation would exceed the limits defined in libc's float.h.
  *
  * The decimal separator is assumed to be a dot character.
- * The comma character is treated as group separator and ignored during parsing.
+ * The comma character is treated as a group separator and ignored during parsing.
  * If you want to choose a different format, use cx_strtof_lc().
  *
  * @param str the string to convert
@@ -2170,13 +2067,13 @@
 #define cx_strtof(str, output) cx_strtof_lc_(cx_strcast(str), output, '.', ",")
 
 /**
- * Converts a string to a double precision floating point number.
+ * Converts a string to a double precision floating-point number.
  *
  * The function returns non-zero when conversion is not possible.
  * In that case the function sets errno to EINVAL when the reason is an invalid character.
  *
  * The decimal separator is assumed to be a dot character.
- * The comma character is treated as group separator and ignored during parsing.
+ * The comma character is treated as a group separator and ignored during parsing.
  * If you want to choose a different format, use cx_strtof_lc().
  *
  * @param str the string to convert
--- a/ucx/cx/test.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/cx/test.h	Sat Nov 08 23:06:11 2025 +0100
@@ -56,7 +56,7 @@
  * }
  * </code>
  * 
- * @attention Do not call own functions within a test, that use
+ * @attention Do not call own functions within a test that use
  * CX_TEST_ASSERT() macros and are not defined by using CX_TEST_SUBROUTINE().
  *
  * @author Mike Becker
@@ -136,10 +136,7 @@
  * @param name optional name of the suite
  * @return a new test suite
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_cstr_arg(1)
-cx_attr_malloc
+cx_attr_nonnull cx_attr_nodiscard  cx_attr_cstr_arg(1) cx_attr_malloc
 static inline CxTestSuite* cx_test_suite_new(const char *name) {
     CxTestSuite* suite = (CxTestSuite*) malloc(sizeof(CxTestSuite));
     if (suite != NULL) {
@@ -157,7 +154,7 @@
  *
  * @param suite the test suite to free
  */
-static inline void cx_test_suite_free(CxTestSuite* suite) {
+CX_INLINE void cx_test_suite_free(CxTestSuite* suite) {
     if (suite == NULL) return;
     CxTestSet *l = suite->tests;
     while (l != NULL) {
@@ -171,13 +168,13 @@
 /**
  * Registers a test function with the specified test suite.
  * 
- * @param suite the suite, the test function shall be added to
+ * @param suite the suite the test function shall be added to
  * @param test the test function to register
  * @retval zero success
  * @retval non-zero failure
  */
 cx_attr_nonnull
-static inline int cx_test_register(CxTestSuite* suite, CxTest test) {
+CX_INLINE int cx_test_register(CxTestSuite* suite, CxTest test) {
     CxTestSet *t = (CxTestSet*) malloc(sizeof(CxTestSet));
     if (t) {
         t->test = test;
@@ -204,8 +201,7 @@
  * @param out_writer the write function writing to @p out_target
  */
 cx_attr_nonnull
-static inline void cx_test_run(CxTestSuite *suite,
-                               void *out_target, cx_write_func out_writer) {
+CX_INLINE void cx_test_run(CxTestSuite *suite, void *out_target, cx_write_func out_writer) {
     if (suite->name == NULL) {
         out_writer("*** Test Suite ***\n", 1, 19, out_target);
     } else {
@@ -263,7 +259,7 @@
  * }
  * @endcode
  *
- * @attention Any CX_TEST_ASSERT() calls must be performed in scope of
+ * @attention Any CX_TEST_ASSERT() calls must be performed in the scope of
  * #CX_TEST_DO.
  */
 #define CX_TEST_DO _writefnc_("Running ", 1, 8, _output_);\
--- a/ucx/cx/tree.h	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/cx/tree.h	Sat Nov 08 23:06:11 2025 +0100
@@ -49,12 +49,12 @@
  *
  * This iterator is not position-aware in a strict sense, as it does not assume
  * a particular order of elements in the tree. However, the iterator keeps track
- * of the number of nodes it has passed in a counter variable.
+ * of the number of nodes it has passed in a counter-variable.
  * Each node, regardless of the number of passes, is counted only once.
  *
  * @note Objects that are pointed to by an iterator are mutable through that
  * iterator. However, if the
- * underlying data structure is mutated by other means than this iterator (e.g.
+ * underlying data structure is mutated by other means than this iterator (e.g.,
  * elements added or removed), the iterator becomes invalid (regardless of what
  * cxIteratorValid() returns).
  *
@@ -71,7 +71,7 @@
     bool skip;
     /**
      * Set to true, when the iterator shall visit a node again
-     * when all it's children have been processed.
+     * when all its children have been processed.
      */
     bool visit_on_exit;
     /**
@@ -97,7 +97,7 @@
      */
     void *node;
     /**
-     * Stores a copy of the next pointer of the visited node.
+     * Stores a copy of the pointer to the successor of the visited node.
      * Allows freeing a node on exit without corrupting the iteration.
      */
     void *node_next;
@@ -155,12 +155,12 @@
  *
  * This iterator is not position-aware in a strict sense, as it does not assume
  * a particular order of elements in the tree. However, the iterator keeps track
- * of the number of nodes it has passed in a counter variable.
+ * of the number of nodes it has passed in a counter-variable.
  * Each node, regardless of the number of passes, is counted only once.
  *
  * @note Objects that are pointed to by an iterator are mutable through that
  * iterator. However, if the
- * underlying data structure is mutated by other means than this iterator (e.g.
+ * underlying data structure is mutated by other means than this iterator (e.g.,
  * elements added or removed), the iterator becomes invalid (regardless of what
  * cxIteratorValid() returns).
  *
@@ -212,24 +212,14 @@
  * @param iter the iterator
  */
 cx_attr_nonnull
-static inline void cxTreeIteratorDispose(CxTreeIterator *iter) {
-    cxFreeDefault(iter->stack);
-    iter->stack = NULL;
-}
+CX_EXPORT void cxTreeIteratorDispose(CxTreeIterator *iter);
 
 /**
  * Releases internal memory of the given tree visitor.
  * @param visitor the visitor
  */
 cx_attr_nonnull
-static inline void cxTreeVisitorDispose(CxTreeVisitor *visitor) {
-    struct cx_tree_visitor_queue_s *q = visitor->queue_next;
-    while (q != NULL) {
-        struct cx_tree_visitor_queue_s *next = q->next;
-        cxFreeDefault(q);
-        q = next;
-    }
-}
+CX_EXPORT void cxTreeVisitorDispose(CxTreeVisitor *visitor);
 
 /**
  * Advises the iterator to skip the subtree below the current node and
@@ -250,7 +240,7 @@
 /**
  * Links a node to a (new) parent.
  *
- * If the node has already a parent, it is unlinked, first.
+ * If the node already has a parent, it is unlinked, first.
  * If the parent has children already, the node is @em appended to the list
  * of all currently existing children.
  *
@@ -265,16 +255,9 @@
  * @see cx_tree_unlink()
  */
 cx_attr_nonnull
-cx_attr_export
-void cx_tree_link(
-        void *parent,
-        void *node,
-        ptrdiff_t loc_parent,
-        ptrdiff_t loc_children,
-        ptrdiff_t loc_last_child,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next
-);
+CX_EXPORT void cx_tree_link(void *parent, void *node,
+        ptrdiff_t loc_parent, ptrdiff_t loc_children, ptrdiff_t loc_last_child,
+        ptrdiff_t loc_prev, ptrdiff_t loc_next);
 
 /**
  * Unlinks a node from its parent.
@@ -291,15 +274,9 @@
  * @see cx_tree_link()
  */
 cx_attr_nonnull
-cx_attr_export
-void cx_tree_unlink(
-        void *node,
-        ptrdiff_t loc_parent,
-        ptrdiff_t loc_children,
-        ptrdiff_t loc_last_child,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next
-);
+CX_EXPORT void cx_tree_unlink(void *node,
+        ptrdiff_t loc_parent, ptrdiff_t loc_children, ptrdiff_t loc_last_child,
+        ptrdiff_t loc_prev, ptrdiff_t loc_next);
 
 /**
  * Macro that can be used instead of the magic value for infinite search depth.
@@ -318,8 +295,8 @@
  * Zero means exact match and a positive number is an implementation defined
  * measure for the distance to an exact match.
  *
- * For example if a tree stores file path information, a node that is
- * describing a parent directory of a filename that is searched, shall
+ * For example, consider a tree that stores file path information.
+ * A node which is describing a parent directory of a searched file shall
  * return a positive number to indicate that a child node might contain the
  * searched item. On the other hand, if the node denotes a path that is not a
  * prefix of the searched filename, the function would return -1 to indicate
@@ -330,9 +307,8 @@
  *
  * @return 0 if the node contains the data,
  * positive if one of the children might contain the data,
- * negative if neither the node, nor the children contains the data
+ * negative if neither the node nor the children contains the data
  */
-cx_attr_nonnull
 typedef int (*cx_tree_search_data_func)(const void *node, const void *data);
 
 
@@ -348,8 +324,8 @@
  * Zero means exact match and a positive number is an implementation defined
  * measure for the distance to an exact match.
  *
- * For example if a tree stores file path information, a node that is
- * describing a parent directory of a filename that is searched, shall
+ * For example, consider a tree that stores file path information.
+ * A node which is describing a parent directory of a searched file shall
  * return a positive number to indicate that a child node might contain the
  * searched item. On the other hand, if the node denotes a path that is not a
  * prefix of the searched filename, the function would return -1 to indicate
@@ -360,19 +336,18 @@
  *
  * @return 0 if @p node contains the same data as @p new_node,
  * positive if one of the children might contain the data,
- * negative if neither the node, nor the children contains the data
+ * negative if neither the node nor the children contains the data
  */
-cx_attr_nonnull
 typedef int (*cx_tree_search_func)(const void *node, const void *new_node);
 
 /**
  * Searches for data in a tree.
  *
- * When the data cannot be found exactly, the search function might return a
- * closest result which might be a good starting point for adding a new node
+ * When the data cannot be found exactly, the search function might return the
+ * closest result, which might be a good starting point for adding a new node
  * to the tree (see also #cx_tree_add()).
  *
- * Depending on the tree structure it is not necessarily guaranteed that the
+ * Depending on the tree structure, it is not necessarily guaranteed that the
  * "closest" match is uniquely defined. This function will search for a node
  * with the best match according to the @p sfunc (meaning: the return value of
  * @p sfunc which is closest to zero). If that is also ambiguous, an arbitrary
@@ -389,27 +364,19 @@
  * could contain the node (but doesn't right now), negative if the tree does not
  * contain any node that might be related to the searched data
  */
-cx_attr_nonnull
-cx_attr_access_w(5)
-cx_attr_export
-int cx_tree_search_data(
-        const void *root,
-        size_t depth,
-        const void *data,
-        cx_tree_search_data_func sfunc,
-        void **result,
-        ptrdiff_t loc_children,
-        ptrdiff_t loc_next
-);
+cx_attr_nonnull cx_attr_access_w(5)
+CX_EXPORT int cx_tree_search_data(const void *root, size_t depth,
+        const void *data, cx_tree_search_data_func sfunc,
+        void **result, ptrdiff_t loc_children, ptrdiff_t loc_next);
 
 /**
  * Searches for a node in a tree.
  *
  * When no node with the same data can be found, the search function might
- * return a closest result which might be a good starting point for adding the
+ * return the closest result, which might be a good starting point for adding the
  * new node to the tree (see also #cx_tree_add()).
  *
- * Depending on the tree structure it is not necessarily guaranteed that the
+ * Depending on the tree structure, it is not necessarily guaranteed that the
  * "closest" match is uniquely defined. This function will search for a node
  * with the best match according to the @p sfunc (meaning: the return value of
  * @p sfunc which is closest to zero). If that is also ambiguous, an arbitrary
@@ -426,18 +393,10 @@
  * could contain the node (but doesn't right now), negative if the tree does not
  * contain any node that might be related to the searched data
  */
-cx_attr_nonnull
-cx_attr_access_w(5)
-cx_attr_export
-int cx_tree_search(
-        const void *root,
-        size_t depth,
-        const void *node,
-        cx_tree_search_func sfunc,
-        void **result,
-        ptrdiff_t loc_children,
-        ptrdiff_t loc_next
-);
+cx_attr_nonnull cx_attr_access_w(5)
+CX_EXPORT int cx_tree_search(const void *root, size_t depth,
+        const void *node, cx_tree_search_func sfunc,
+        void **result, ptrdiff_t loc_children, ptrdiff_t loc_next);
 
 /**
  * Creates a depth-first iterator for a tree with the specified root node.
@@ -460,13 +419,8 @@
  * @see cxTreeIteratorDispose()
  */
 cx_attr_nodiscard
-cx_attr_export
-CxTreeIterator cx_tree_iterator(
-        void *root,
-        bool visit_on_exit,
-        ptrdiff_t loc_children,
-        ptrdiff_t loc_next
-);
+CX_EXPORT CxTreeIterator cx_tree_iterator(void *root, bool visit_on_exit,
+        ptrdiff_t loc_children, ptrdiff_t loc_next);
 
 /**
  * Creates a breadth-first iterator for a tree with the specified root node.
@@ -487,24 +441,19 @@
  * @see cxTreeVisitorDispose()
  */
 cx_attr_nodiscard
-cx_attr_export
-CxTreeVisitor cx_tree_visitor(
-        void *root,
-        ptrdiff_t loc_children,
-        ptrdiff_t loc_next
-);
+CX_EXPORT CxTreeVisitor cx_tree_visitor(void *root,
+        ptrdiff_t loc_children, ptrdiff_t loc_next);
 
 /**
  * Describes a function that creates a tree node from the specified data.
- * The first argument points to the data the node shall contain and
- * the second argument may be used for additional data (e.g. an allocator).
+ * The first argument points to the data the node shall contain, and
+ * the second argument may be used for additional data (e.g., an allocator).
  * Functions of this type shall either return a new pointer to a newly
  * created node or @c NULL when allocation fails.
  *
  * @note the function may leave the node pointers in the struct uninitialized.
  * The caller is responsible to set them according to the intended use case.
  */
-cx_attr_nonnull_arg(1)
 typedef void *(*cx_tree_node_create_func)(const void *, void *);
 
 /**
@@ -513,8 +462,7 @@
  * This variable is used by #cx_tree_add_array() and #cx_tree_add_iter() to
  * implement optimized insertion of multiple elements into a tree.
  */
-cx_attr_export
-extern unsigned int cx_tree_add_look_around_depth;
+CX_EXPORT extern unsigned int cx_tree_add_look_around_depth;
 
 /**
  * Adds multiple elements efficiently to a tree.
@@ -554,23 +502,12 @@
  * @return the number of nodes created and added
  * @see cx_tree_add()
  */
-cx_attr_nonnull_arg(1, 3, 4, 6, 7)
-cx_attr_access_w(6)
-cx_attr_export
-size_t cx_tree_add_iter(
-        struct cx_iterator_base_s *iter,
-        size_t num,
-        cx_tree_search_func sfunc,
-        cx_tree_node_create_func cfunc,
-        void *cdata,
-        void **failed,
-        void *root,
-        ptrdiff_t loc_parent,
-        ptrdiff_t loc_children,
-        ptrdiff_t loc_last_child,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next
-);
+cx_attr_nonnull_arg(1, 3, 4, 6, 7) cx_attr_access_w(6)
+CX_EXPORT size_t cx_tree_add_iter(struct cx_iterator_base_s *iter, size_t num,
+        cx_tree_search_func sfunc, cx_tree_node_create_func cfunc,
+        void *cdata, void **failed, void *root,
+        ptrdiff_t loc_parent, ptrdiff_t loc_children, ptrdiff_t loc_last_child,
+        ptrdiff_t loc_prev, ptrdiff_t loc_next);
 
 /**
  * Adds multiple elements efficiently to a tree.
@@ -609,24 +546,12 @@
  * @return the number of array elements successfully processed
  * @see cx_tree_add()
  */
-cx_attr_nonnull_arg(1, 4, 5, 7, 8)
-cx_attr_access_w(7)
-cx_attr_export
-size_t cx_tree_add_array(
-        const void *src,
-        size_t num,
-        size_t elem_size,
-        cx_tree_search_func sfunc,
-        cx_tree_node_create_func cfunc,
-        void *cdata,
-        void **failed,
-        void *root,
-        ptrdiff_t loc_parent,
-        ptrdiff_t loc_children,
-        ptrdiff_t loc_last_child,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next
-);
+cx_attr_nonnull_arg(1, 4, 5, 7, 8) cx_attr_access_w(7)
+CX_EXPORT size_t cx_tree_add_array(const void *src, size_t num, size_t elem_size,
+        cx_tree_search_func sfunc, cx_tree_node_create_func cfunc,
+        void *cdata, void **failed, void *root,
+        ptrdiff_t loc_parent, ptrdiff_t loc_children, ptrdiff_t loc_last_child,
+        ptrdiff_t loc_prev, ptrdiff_t loc_next);
 
 /**
  * Adds data to a tree.
@@ -637,17 +562,17 @@
  * When a location is found, the @p cfunc will be invoked with @p cdata.
  *
  * The node returned by @p cfunc will be linked into the tree.
- * When @p sfunc returned a positive integer, the new node will be linked as a
+ * When @p sfunc returns a positive integer, the new node will be linked as a
  * child. The other children (now siblings of the new node) are then checked
  * with @p sfunc, whether they could be children of the new node and re-linked
  * accordingly.
  *
- * When @p sfunc returned zero and the found node has a parent, the new
- * node will be added as sibling - otherwise, the new node will be added
+ * When @p sfunc returns zero and the found node has a parent, the new
+ * node will be added as a sibling - otherwise, the new node will be added
  * as a child.
  *
- * When @p sfunc returned a negative value, the new node will not be added to
- * the tree and this function returns a non-zero value.
+ * When @p sfunc returns a negative value, the new node will not be added to
+ * the tree, and this function returns a non-zero value.
  * The caller should check if @p cnode contains a node pointer and deal with the
  * node that could not be added.
  *
@@ -673,22 +598,12 @@
  * @return zero when a new node was created and added to the tree,
  * non-zero otherwise
  */
-cx_attr_nonnull_arg(1, 2, 3, 5, 6)
-cx_attr_access_w(5)
-cx_attr_export
-int cx_tree_add(
-        const void *src,
-        cx_tree_search_func sfunc,
-        cx_tree_node_create_func cfunc,
-        void *cdata,
-        void **cnode,
-        void *root,
-        ptrdiff_t loc_parent,
-        ptrdiff_t loc_children,
-        ptrdiff_t loc_last_child,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next
-);
+cx_attr_nonnull_arg(1, 2, 3, 5, 6) cx_attr_access_w(5)
+CX_EXPORT int cx_tree_add(const void *src,
+        cx_tree_search_func sfunc, cx_tree_node_create_func cfunc,
+        void *cdata, void **cnode, void *root,
+        ptrdiff_t loc_parent, ptrdiff_t loc_children, ptrdiff_t loc_last_child,
+        ptrdiff_t loc_prev, ptrdiff_t loc_next);
 
 
 /**
@@ -747,9 +662,9 @@
      * A function to create new nodes.
      *
      * Invocations to this function will receive a pointer to this tree
-     * structure as second argument.
+     * structure as the second argument.
      *
-     * Nodes MAY use #cx_tree_node_base_s as base layout, but do not need to.
+     * Nodes MAY use #cx_tree_node_base_s as the base layout, but do not need to.
      */
     cx_tree_node_create_func node_create;
 
@@ -814,7 +729,7 @@
  * Macro to roll out the #cx_tree_node_base_s structure with a custom
  * node type.
  *
- * Must be used as first member in your custom tree struct.
+ * Must be used as the first member in your custom tree struct.
  *
  * @param type the data type for the nodes
  */
@@ -850,32 +765,20 @@
      * Implementations SHALL NOT simply invoke @p insert_many as this comes
      * with too much overhead.
      */
-    int (*insert_element)(
-            struct cx_tree_s *tree,
-            const void *data
-    );
+    int (*insert_element)(struct cx_tree_s *tree, const void *data);
 
     /**
      * Member function for inserting multiple elements.
      *
-     * Implementations SHALL avoid to perform a full search in the tree for
+     * Implementations SHALL avoid performing a full search in the tree for
      * every element even though the source data MAY be unsorted.
      */
-    size_t (*insert_many)(
-            struct cx_tree_s *tree,
-            struct cx_iterator_base_s *iter,
-            size_t n
-    );
+    size_t (*insert_many)(struct cx_tree_s *tree, struct cx_iterator_base_s *iter, size_t n);
 
     /**
      * Member function for finding a node.
      */
-    void *(*find)(
-            struct cx_tree_s *tree,
-            const void *subtree,
-            const void *data,
-            size_t depth
-    );
+    void *(*find)(struct cx_tree_s *tree, const void *subtree, const void *data, size_t depth);
 };
 
 /**
@@ -885,7 +788,7 @@
 
 
 /**
- * Destroys a node and it's subtree.
+ * Destroys a node and its subtree.
  *
  * It is guaranteed that the simple destructor is invoked before
  * the advanced destructor, starting with the leaf nodes of the subtree.
@@ -906,8 +809,7 @@
  * @see cxTreeFree()
  */
 cx_attr_nonnull
-cx_attr_export
-void cxTreeDestroySubtree(CxTree *tree, void *node);
+CX_EXPORT void cxTreeDestroySubtree(CxTree *tree, void *node);
 
 
 /**
@@ -921,7 +823,7 @@
  *
  * @attention Be careful when calling this function when no destructor function
  * is registered that actually frees the memory of nodes. In that case you will
- * need a reference to the (former) root node of the tree somewhere or
+ * need a reference to the (former) root node of the tree somewhere, or
  * otherwise you will be leaking memory.
  *
  * @param tree the tree
@@ -945,8 +847,7 @@
  *
  * @param tree the tree to free
  */
-cx_attr_export
-void cxTreeFree(CxTree *tree);
+CX_EXPORT void cxTreeFree(CxTree *tree);
 
 /**
  * Creates a new tree structure based on the specified layout.
@@ -972,29 +873,18 @@
  * @see cxTreeCreateSimple()
  * @see cxTreeCreateWrapped()
  */
-cx_attr_nonnull_arg(2, 3, 4)
-cx_attr_nodiscard
-cx_attr_malloc
-cx_attr_dealloc(cxTreeFree, 1)
-cx_attr_export
-CxTree *cxTreeCreate(
-        const CxAllocator *allocator,
-        cx_tree_node_create_func create_func,
-        cx_tree_search_func search_func,
-        cx_tree_search_data_func search_data_func,
-        ptrdiff_t loc_parent,
-        ptrdiff_t loc_children,
-        ptrdiff_t loc_last_child,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next
-);
+cx_attr_nonnull_arg(2, 3, 4) cx_attr_nodiscard  cx_attr_malloc cx_attr_dealloc(cxTreeFree, 1)
+CX_EXPORT CxTree *cxTreeCreate(const CxAllocator *allocator, cx_tree_node_create_func create_func,
+        cx_tree_search_func search_func, cx_tree_search_data_func search_data_func,
+        ptrdiff_t loc_parent, ptrdiff_t loc_children, ptrdiff_t loc_last_child,
+        ptrdiff_t loc_prev, ptrdiff_t loc_next);
 
 /**
  * Creates a new tree structure based on a default layout.
  *
- * Nodes created by @p create_func MUST contain #cx_tree_node_base_s as first
+ * Nodes created by @p create_func MUST contain #cx_tree_node_base_s as the first
  * member (or at least respect the default offsets specified in the tree
- * struct) and they MUST be allocated with the specified allocator.
+ * struct), and they MUST be allocated with the specified allocator.
  *
  * @note This function will also register an advanced destructor which
  * will free the nodes with the allocator's free() method.
@@ -1006,10 +896,8 @@
  * @return (@c CxTree*) the new tree
  * @see cxTreeCreate()
  */
-#define cxTreeCreateSimple(\
-    allocator, create_func, search_func, search_data_func \
-) cxTreeCreate(allocator, create_func, search_func, search_data_func, \
-cx_tree_node_base_layout)
+#define cxTreeCreateSimple(allocator, create_func, search_func, search_data_func) \
+        cxTreeCreate(allocator, create_func, search_func, search_data_func, cx_tree_node_base_layout)
 
 /**
  * Creates a new tree structure based on an existing tree.
@@ -1019,7 +907,7 @@
  * @attention This function will create an incompletely defined tree structure
  * where neither the create function, the search function, nor a destructor
  * will be set. If you wish to use any of this functionality for the wrapped
- * tree, you need to specify those functions afterwards.
+ * tree, you need to specify those functions afterward.
  *
  * @param allocator the allocator that was used for nodes of the wrapped tree
  * (if @c NULL, the cxDefaultAllocator is assumed)
@@ -1033,20 +921,10 @@
  * @return the new tree
  * @see cxTreeCreate()
  */
-cx_attr_nonnull_arg(2)
-cx_attr_nodiscard
-cx_attr_malloc
-cx_attr_dealloc(cxTreeFree, 1)
-cx_attr_export
-CxTree *cxTreeCreateWrapped(
-        const CxAllocator *allocator,
-        void *root,
-        ptrdiff_t loc_parent,
-        ptrdiff_t loc_children,
-        ptrdiff_t loc_last_child,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next
-);
+cx_attr_nonnull_arg(2) cx_attr_nodiscard  cx_attr_malloc  cx_attr_dealloc(cxTreeFree, 1)
+CX_EXPORT CxTree *cxTreeCreateWrapped(const CxAllocator *allocator, void *root,
+        ptrdiff_t loc_parent, ptrdiff_t loc_children, ptrdiff_t loc_last_child,
+        ptrdiff_t loc_prev, ptrdiff_t loc_next);
 
 /**
  * Inserts data into the tree.
@@ -1061,12 +939,7 @@
  * @retval non-zero failure
  */
 cx_attr_nonnull
-static inline int cxTreeInsert(
-        CxTree *tree,
-        const void *data
-) {
-    return tree->cl->insert_element(tree, data);
-}
+CX_EXPORT int cxTreeInsert(CxTree *tree, const void *data);
 
 /**
  * Inserts elements provided by an iterator efficiently into the tree.
@@ -1081,13 +954,7 @@
  * @return the number of elements that could be successfully inserted
  */
 cx_attr_nonnull
-static inline size_t cxTreeInsertIter(
-        CxTree *tree,
-        CxIteratorBase *iter,
-        size_t n
-) {
-    return tree->cl->insert_many(tree, iter, n);
-}
+CX_EXPORT size_t cxTreeInsertIter(CxTree *tree, CxIteratorBase *iter, size_t n);
 
 /**
  * Inserts an array of data efficiently into the tree.
@@ -1103,17 +970,7 @@
  * @return the number of elements that could be successfully inserted
  */
 cx_attr_nonnull
-static inline size_t cxTreeInsertArray(
-        CxTree *tree,
-        const void *data,
-        size_t elem_size,
-        size_t n
-) {
-    if (n == 0) return 0;
-    if (n == 1) return 0 == cxTreeInsert(tree, data) ? 1 : 0;
-    CxIterator iter = cxIterator(data, elem_size, n);
-    return cxTreeInsertIter(tree, cxIteratorRef(iter), n);
-}
+CX_EXPORT size_t cxTreeInsertArray(CxTree *tree, const void *data, size_t elem_size, size_t n);
 
 /**
  * Searches the data in the specified tree.
@@ -1126,14 +983,8 @@
  * @param data the data to search for
  * @return the first matching node, or @c NULL when the data cannot be found
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-static inline void *cxTreeFind(
-        CxTree *tree,
-        const void *data
-) {
-    return tree->cl->find(tree, tree->root, data, 0);
-}
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT void *cxTreeFind(CxTree *tree, const void *data);
 
 /**
  * Searches the data in the specified subtree.
@@ -1154,16 +1005,8 @@
  * @param max_depth the maximum search depth
  * @return the first matching node, or @c NULL when the data cannot be found
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-static inline void *cxTreeFindInSubtree(
-        CxTree *tree,
-        const void *data,
-        void *subtree_root,
-        size_t max_depth
-) {
-    return tree->cl->find(tree, subtree_root, data, max_depth);
-}
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT void *cxTreeFindInSubtree(CxTree *tree, const void *data, void *subtree_root, size_t max_depth);
 
 /**
  * Determines the size of the specified subtree.
@@ -1172,10 +1015,8 @@
  * @param subtree_root the root node of the subtree
  * @return the number of nodes in the specified subtree
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-size_t cxTreeSubtreeSize(CxTree *tree, void *subtree_root);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT size_t cxTreeSubtreeSize(CxTree *tree, void *subtree_root);
 
 /**
  * Determines the depth of the specified subtree.
@@ -1184,10 +1025,8 @@
  * @param subtree_root the root node of the subtree
  * @return the tree depth including the @p subtree_root
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-size_t cxTreeSubtreeDepth(CxTree *tree, void *subtree_root);
+cx_attr_nonnull  cx_attr_nodiscard
+CX_EXPORT size_t cxTreeSubtreeDepth(CxTree *tree, void *subtree_root);
 
 /**
  * Determines the size of the entire tree.
@@ -1195,11 +1034,8 @@
  * @param tree the tree
  * @return the tree size, counting the root as one
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-static inline size_t cxTreeSize(CxTree *tree) {
-    return tree->size;
-}
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT size_t cxTreeSize(CxTree *tree);
 
 /**
  * Determines the depth of the entire tree.
@@ -1207,10 +1043,8 @@
  * @param tree the tree
  * @return the tree depth, counting the root as one
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-cx_attr_export
-size_t cxTreeDepth(CxTree *tree);
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT size_t cxTreeDepth(CxTree *tree);
 
 /**
  * Creates a depth-first iterator for the specified tree starting in @p node.
@@ -1224,18 +1058,8 @@
  * @return a tree iterator (depth-first)
  * @see cxTreeVisit()
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-static inline CxTreeIterator cxTreeIterateSubtree(
-        CxTree *tree,
-        void *node,
-        bool visit_on_exit
-) {
-    return cx_tree_iterator(
-            node, visit_on_exit,
-            tree->loc_children, tree->loc_next
-    );
-}
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT CxTreeIterator cxTreeIterateSubtree(CxTree *tree, void *node, bool visit_on_exit);
 
 /**
  * Creates a breadth-first iterator for the specified tree starting in @p node.
@@ -1247,13 +1071,8 @@
  * @return a tree visitor (a.k.a. breadth-first iterator)
  * @see cxTreeIterate()
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-static inline CxTreeVisitor cxTreeVisitSubtree(CxTree *tree, void *node) {
-    return cx_tree_visitor(
-            node, tree->loc_children, tree->loc_next
-    );
-}
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT CxTreeVisitor cxTreeVisitSubtree(CxTree *tree, void *node);
 
 /**
  * Creates a depth-first iterator for the specified tree.
@@ -1264,14 +1083,8 @@
  * @return a tree iterator (depth-first)
  * @see cxTreeVisit()
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-static inline CxTreeIterator cxTreeIterate(
-        CxTree *tree,
-        bool visit_on_exit
-) {
-    return cxTreeIterateSubtree(tree, tree->root, visit_on_exit);
-}
+cx_attr_nonnull cx_attr_nodiscard
+CX_EXPORT CxTreeIterator cxTreeIterate(CxTree *tree, bool visit_on_exit);
 
 /**
  * Creates a breadth-first iterator for the specified tree.
@@ -1280,16 +1093,13 @@
  * @return a tree visitor (a.k.a. breadth-first iterator)
  * @see cxTreeIterate()
  */
-cx_attr_nonnull
-cx_attr_nodiscard
-static inline CxTreeVisitor cxTreeVisit(CxTree *tree) {
-    return cxTreeVisitSubtree(tree, tree->root);
-}
+cx_attr_nonnull cx_attr_nodiscard
+CxTreeVisitor cxTreeVisit(CxTree *tree);
 
 /**
  * Sets the (new) parent of the specified child.
  *
- * If the @p child is not already member of the tree, this function behaves
+ * If the @p child is not already a member of the tree, this function behaves
  * as #cxTreeAddChildNode().
  *
  * @param tree the tree
@@ -1298,21 +1108,16 @@
  * @see cxTreeAddChildNode()
  */
 cx_attr_nonnull
-cx_attr_export
-void cxTreeSetParent(
-        CxTree *tree,
-        void *parent,
-        void *child
-);
+CX_EXPORT void cxTreeSetParent(CxTree *tree, void *parent, void *child);
 
 /**
  * Adds a new node to the tree.
  *
- * If the @p child is already member of the tree, the behavior is undefined.
+ * If the @p child is already a member of the tree, the behavior is undefined.
  * Use #cxTreeSetParent() if you want to move a subtree to another location.
  *
  * @attention The node may be externally created, but MUST obey the same rules
- * as if it was created by the tree itself with #cxTreeAddChild() (e.g. use
+ * as if it was created by the tree itself with #cxTreeAddChild() (e.g., use
  * the same allocator).
  *
  * @param tree the tree
@@ -1321,12 +1126,7 @@
  * @see cxTreeSetParent()
  */
 cx_attr_nonnull
-cx_attr_export
-void cxTreeAddChildNode(
-        CxTree *tree,
-        void *parent,
-        void *child
-);
+CX_EXPORT void cxTreeAddChildNode(CxTree *tree, void *parent, void *child);
 
 /**
  * Creates a new node and adds it to the tree.
@@ -1336,7 +1136,7 @@
  * leaving this task to the tree by using #cxTreeInsert().
  *
  * Be aware that adding nodes at arbitrary locations in the tree might cause
- * wrong or undesired results when subsequently invoking #cxTreeInsert() and
+ * wrong or undesired results when subsequently invoking #cxTreeInsert(), and
  * the invariant imposed by the search function does not hold any longer.
  *
  * @param tree the tree
@@ -1346,12 +1146,7 @@
  * @see cxTreeInsert()
  */
 cx_attr_nonnull
-cx_attr_export
-int cxTreeAddChild(
-        CxTree *tree,
-        void *parent,
-        const void *data
-);
+CX_EXPORT int cxTreeAddChild(CxTree *tree, void *parent, const void *data);
 
 /**
  * A function that is invoked when a node needs to be re-linked to a new parent.
@@ -1365,7 +1160,6 @@
  * @param old_parent the old parent of the node
  * @param new_parent the new parent of the node
  */
-cx_attr_nonnull
 typedef void (*cx_tree_relink_func)(
         void *node,
         const void *old_parent,
@@ -1387,15 +1181,10 @@
  * @return zero on success, non-zero if @p node is the root node of the tree
  */
 cx_attr_nonnull_arg(1, 2)
-cx_attr_export
-int cxTreeRemoveNode(
-        CxTree *tree,
-        void *node,
-        cx_tree_relink_func relink_func
-);
+CX_EXPORT int cxTreeRemoveNode(CxTree *tree, void *node, cx_tree_relink_func relink_func);
 
 /**
- * Removes a node and it's subtree from the tree.
+ * Removes a node and its subtree from the tree.
  *
  * If the node is not part of the tree, the behavior is undefined.
  *
@@ -1406,8 +1195,7 @@
  * @param node the node to remove
  */
 cx_attr_nonnull
-cx_attr_export
-void cxTreeRemoveSubtree(CxTree *tree, void *node);
+CX_EXPORT void cxTreeRemoveSubtree(CxTree *tree, void *node);
 
 /**
  * Destroys a node and re-links its children to its former parent.
@@ -1428,12 +1216,7 @@
  * @return zero on success, non-zero if @p node is the root node of the tree
  */
 cx_attr_nonnull_arg(1, 2)
-cx_attr_export
-int cxTreeDestroyNode(
-        CxTree *tree,
-        void *node,
-        cx_tree_relink_func relink_func
-);
+CX_EXPORT int cxTreeDestroyNode(CxTree *tree, void *node, cx_tree_relink_func relink_func);
 
 #ifdef __cplusplus
 } // extern "C"
--- a/ucx/hash_key.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/hash_key.c	Sat Nov 08 23:06:11 2025 +0100
@@ -27,6 +27,7 @@
  */
 
 #include "cx/hash_key.h"
+#include "cx/compare.h"
 #include <string.h>
 
 void cx_hash_murmur(CxHashKey *key) {
@@ -81,6 +82,21 @@
     key->hash = h;
 }
 
+
+uint32_t cx_hash_u32(uint32_t x) {
+    x = ((x >> 16) ^ x) * 0x45d9f3bu;
+    x = ((x >> 16) ^ x) * 0x45d9f3bu;
+    x = (x >> 16) ^ x;
+    return x;
+}
+
+uint64_t cx_hash_u64(uint64_t x) {
+    x = (x ^ (x >> 30)) * UINT64_C(0xbf58476d1ce4e5b9);
+    x = (x ^ (x >> 27)) * UINT64_C(0x94d049bb133111eb);
+    x = x ^ (x >> 31);
+    return x;
+}
+
 CxHashKey cx_hash_key_str(const char *str) {
     CxHashKey key;
     key.data = str;
@@ -89,6 +105,22 @@
     return key;
 }
 
+CxHashKey cx_hash_key_ustr(unsigned const char *str) {
+    CxHashKey key;
+    key.data = str;
+    key.len = str == NULL ? 0 : strlen((const char*)str);
+    cx_hash_murmur(&key);
+    return key;
+}
+
+CxHashKey cx_hash_key_cxstr(cxstring str) {
+    return cx_hash_key(str.ptr, str.length);
+}
+
+CxHashKey cx_hash_key_mutstr(cxmutstr str) {
+    return cx_hash_key(str.ptr, str.length);
+}
+
 CxHashKey cx_hash_key_bytes(
         const unsigned char *bytes,
         size_t len
@@ -110,3 +142,31 @@
     cx_hash_murmur(&key);
     return key;
 }
+
+CxHashKey cx_hash_key_u32(uint32_t x) {
+    CxHashKey key;
+    key.data = NULL;
+    key.len = 0;
+    key.hash = cx_hash_u32(x);
+    return key;
+}
+
+CxHashKey cx_hash_key_u64(uint64_t x) {
+    CxHashKey key;
+    key.data = NULL;
+    key.len = 0;
+    key.hash = cx_hash_u64(x);
+    return key;
+}
+
+int cx_hash_key_cmp(const void *l, const void *r) {
+    const CxHashKey *left = l;
+    const CxHashKey *right = r;
+    int d;
+    d = cx_vcmp_uint64(left->hash, right->hash);
+    if (d != 0) return d;
+    d = cx_vcmp_size(left->len, right->len);
+    if (d != 0) return d;
+    if (left->len == 0) return 0;
+    return memcmp(left->data, right->data, left->len);
+}
--- a/ucx/hash_map.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/hash_map.c	Sat Nov 08 23:06:11 2025 +0100
@@ -86,7 +86,7 @@
     struct cx_hash_map_s *hash_map = (struct cx_hash_map_s *) map;
     const CxAllocator *allocator = map->collection.allocator;
 
-    unsigned hash = key.hash;
+    uint64_t hash = key.hash;
     if (hash == 0) {
         cx_hash_murmur(&key);
         hash = key.hash;
@@ -101,8 +101,7 @@
         elm = elm->next;
     }
 
-    if (elm != NULL && elm->key.hash == hash && elm->key.len == key.len &&
-        memcmp(elm->key.data, key.data, key.len) == 0) {
+    if (elm != NULL && cx_hash_key_cmp(&elm->key, &key) == 0) {
         // overwrite existing element, but call destructors first
         cx_invoke_destructor(map, elm->data);
         if (value == NULL) {
@@ -204,7 +203,7 @@
 ) {
     struct cx_hash_map_s *hash_map = (struct cx_hash_map_s *) map;
 
-    unsigned hash = key.hash;
+    uint64_t hash = key.hash;
     if (hash == 0) {
         cx_hash_murmur(&key);
         hash = key.hash;
@@ -214,27 +213,25 @@
     struct cx_hash_map_element_s *elm = hash_map->buckets[slot];
     struct cx_hash_map_element_s *prev = NULL;
     while (elm && elm->key.hash <= hash) {
-        if (elm->key.hash == hash && elm->key.len == key.len) {
-            if (memcmp(elm->key.data, key.data, key.len) == 0) {
-                if (remove) {
-                    if (targetbuf == NULL) {
-                        cx_invoke_destructor(map, elm->data);
-                    } else {
-                        memcpy(targetbuf, elm->data, map->collection.elem_size);
-                    }
-                    cx_hash_map_unlink(hash_map, slot, prev, elm);
+        if (cx_hash_key_cmp(&elm->key, &key) == 0) {
+            if (remove) {
+                if (targetbuf == NULL) {
+                    cx_invoke_destructor(map, elm->data);
                 } else {
-                    assert(targetbuf != NULL);
-                    void *data = NULL;
-                    if (map->collection.store_pointer) {
-                        data = *(void **) elm->data;
-                    } else {
-                        data = elm->data;
-                    }
-                    memcpy(targetbuf, &data, sizeof(void *));
+                    memcpy(targetbuf, elm->data, map->collection.elem_size);
                 }
-                return 0;
+                cx_hash_map_unlink(hash_map, slot, prev, elm);
+            } else {
+                assert(targetbuf != NULL);
+                void *data = NULL;
+                if (map->collection.store_pointer) {
+                    data = *(void **) elm->data;
+                } else {
+                    data = elm->data;
+                }
+                memcpy(targetbuf, &data, sizeof(void *));
             }
+            return 0;
         }
         prev = elm;
         elm = prev->next;
@@ -269,19 +266,12 @@
 
 static void *cx_hash_map_iter_current_key(const void *it) {
     const CxMapIterator *iter = it;
-    struct cx_hash_map_element_s *elm = iter->elem;
-    return &elm->key;
+    return (void*) iter->entry.key;
 }
 
 static void *cx_hash_map_iter_current_value(const void *it) {
     const CxMapIterator *iter = it;
-    const CxMap *map = iter->map.c;
-    struct cx_hash_map_element_s *elm = iter->elem;
-    if (map->collection.store_pointer) {
-        return *(void **) elm->data;
-    } else {
-        return elm->data;
-    }
+    return iter->entry.value;
 }
 
 static bool cx_hash_map_iter_valid(const void *it) {
@@ -291,7 +281,7 @@
 
 static void cx_hash_map_iter_next(void *it) {
     CxMapIterator *iter = it;
-    CxMap *map = iter->map.m;
+    CxMap *map = iter->map;
     struct cx_hash_map_s *hmap = (struct cx_hash_map_s *) map;
     struct cx_hash_map_element_s *elm = iter->elem;
 
@@ -318,6 +308,7 @@
 
         // unlink
         cx_hash_map_unlink(hmap, iter->slot, prev, elm);
+        iter->elem_count--;
 
         // advance
         elm = next;
@@ -338,7 +329,7 @@
     // must not modify the iterator (the parameter is const)
     if (elm != NULL) {
         iter->entry.key = &elm->key;
-        if (iter->map.c->collection.store_pointer) {
+        if (map->collection.store_pointer) {
             iter->entry.value = *(void **) elm->data;
         } else {
             iter->entry.value = elm->data;
@@ -352,7 +343,7 @@
 ) {
     CxMapIterator iter;
 
-    iter.map.c = map;
+    iter.map = (CxMap*) map;
     iter.elem_count = map->collection.size;
 
     switch (type) {
@@ -375,7 +366,7 @@
     iter.base.valid = cx_hash_map_iter_valid;
     iter.base.next = cx_hash_map_iter_next;
     iter.base.remove = false;
-    iter.base.mutating = false;
+    iter.base.allow_remove = true;
 
     iter.slot = 0;
     iter.index = 0;
--- a/ucx/iterator.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/iterator.c	Sat Nov 08 23:06:11 2025 +0100
@@ -53,7 +53,7 @@
         // only move the last element when we are not currently aiming
         // at the last element already
         if (iter->index < iter->elem_count) {
-            void *last = ((char *) iter->src_handle.m)
+            void *last = ((char *) iter->src_handle)
                          + iter->elem_count * iter->elem_size;
             memcpy(iter->elem_handle, last, iter->elem_size);
         }
@@ -84,8 +84,8 @@
     }
 }
 
-CxIterator cxMutIterator(
-        void *array,
+CxIterator cxIterator(
+        const void *array,
         size_t elem_size,
         size_t elem_count,
         bool remove_keeps_order
@@ -93,44 +93,25 @@
     CxIterator iter;
 
     iter.index = 0;
-    iter.src_handle.m = array;
-    iter.elem_handle = array;
+    iter.src_handle = (void*) array;
+    iter.elem_handle = (void*) array;
     iter.elem_size = elem_size;
     iter.elem_count = array == NULL ? 0 : elem_count;
     iter.base.valid = cx_iter_valid;
     iter.base.current = cx_iter_current;
     iter.base.next = remove_keeps_order ? cx_iter_next_slow : cx_iter_next_fast;
     iter.base.remove = false;
-    iter.base.mutating = true;
-
-    return iter;
-}
+    iter.base.allow_remove = true;
 
-CxIterator cxIterator(
-        const void *array,
-        size_t elem_size,
-        size_t elem_count
-) {
-    CxIterator iter = cxMutIterator((void*)array, elem_size, elem_count, false);
-    iter.base.mutating = false;
-    return iter;
-}
-
-CxIterator cxMutIteratorPtr(
-        void *array,
-        size_t elem_count,
-        bool remove_keeps_order
-) {
-    CxIterator iter = cxMutIterator(array, sizeof(void*), elem_count, remove_keeps_order);
-    iter.base.current = cx_iter_current_ptr;
     return iter;
 }
 
 CxIterator cxIteratorPtr(
         const void *array,
-        size_t elem_count
+        size_t elem_count,
+        bool remove_keeps_order
 ) {
-    CxIterator iter = cxMutIteratorPtr((void*) array, elem_count, false);
-    iter.base.mutating = false;
+    CxIterator iter = cxIterator(array, sizeof(void*), elem_count, remove_keeps_order);
+    iter.base.current = cx_iter_current_ptr;
     return iter;
 }
--- a/ucx/json.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/json.c	Sat Nov 08 23:06:11 2025 +0100
@@ -32,6 +32,7 @@
 #include <assert.h>
 #include <stdio.h>
 #include <inttypes.h>
+#include <ctype.h>
 
 /*
  * RFC 8259
@@ -127,16 +128,6 @@
     }
 }
 
-static bool json_isdigit(char c) {
-    // TODO: remove once UCX has public API for this
-    return c >= '0' && c <= '9';
-}
-
-static bool json_isspace(char c) {
-    // TODO: remove once UCX has public API for this
-    return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\v' || c == '\f';
-}
-
 static int num_isexp(const char *content, size_t length, size_t pos) {
     if (pos >= length) {
         return 0;
@@ -145,7 +136,7 @@
     int ok = 0;
     for (size_t i = pos; i < length; i++) {
         char c = content[i];
-        if (json_isdigit(c)) {
+        if (isdigit((unsigned char)c)) {
             ok = 1;
         } else if (i == pos) {
             if (!(c == '+' || c == '-')) {
@@ -162,7 +153,7 @@
 static CxJsonTokenType token_numbertype(const char *content, size_t length) {
     if (length == 0) return CX_JSON_TOKEN_ERROR;
 
-    if (content[0] != '-' && !json_isdigit(content[0])) {
+    if (content[0] != '-' && !isdigit((unsigned char)content[0])) {
         return CX_JSON_TOKEN_ERROR;
     }
 
@@ -175,7 +166,7 @@
             type = CX_JSON_TOKEN_NUMBER;
         } else if (content[i] == 'e' || content[i] == 'E') {
             return num_isexp(content, length, i + 1) ? CX_JSON_TOKEN_NUMBER : CX_JSON_TOKEN_ERROR;
-        } else if (!json_isdigit(content[i])) {
+        } else if (!isdigit((unsigned char)content[i])) {
             return CX_JSON_TOKEN_ERROR; // char is not a digit, decimal separator or exponent sep
         }
     }
@@ -239,7 +230,7 @@
             return CX_JSON_TOKEN_STRING;
         }
         default: {
-            if (json_isspace(c)) {
+            if (isspace((unsigned char)c)) {
                 return CX_JSON_TOKEN_SPACE;
             }
         }
@@ -639,6 +630,12 @@
     }
 }
 
+void cxJsonReset(CxJson *json) {
+    const CxAllocator *allocator = json->allocator;
+    cxJsonDestroy(json);
+    cxJsonInit(json, allocator);
+}
+
 int cxJsonFilln(CxJson *json, const char *buf, size_t size) {
     if (cxBufferEof(&json->buffer)) {
         // reinitialize the buffer
@@ -1135,10 +1132,39 @@
     return ret;
 }
 
+char *cxJsonAsString(const CxJsonValue *value) {
+    return value->value.string.ptr;
+}
+
+cxstring cxJsonAsCxString(const CxJsonValue *value) {
+    return cx_strcast(value->value.string);
+}
+
+cxmutstr cxJsonAsCxMutStr(const CxJsonValue *value) {
+    return value->value.string;
+}
+
+double cxJsonAsDouble(const CxJsonValue *value) {
+    if (value->type == CX_JSON_INTEGER) {
+        return (double) value->value.integer;
+    } else {
+        return value->value.number;
+    }
+}
+
+int64_t cxJsonAsInteger(const CxJsonValue *value) {
+    if (value->type == CX_JSON_INTEGER) {
+        return value->value.integer;
+    } else {
+        return (int64_t) value->value.number;
+    }
+}
+
 CxIterator cxJsonArrIter(const CxJsonValue *value) {
     return cxIteratorPtr(
         value->value.array.array,
-        value->value.array.array_size
+        value->value.array.array_size,
+        true // arrays need to keep order
     );
 }
 
@@ -1146,11 +1172,12 @@
     return cxIterator(
         value->value.object.values,
         sizeof(CxJsonObjValue),
-        value->value.object.values_size
+        value->value.object.values_size,
+        true // TODO: objects do not always need to keep order
     );
 }
 
-CxJsonValue *cx_json_obj_get_cxstr(const CxJsonValue *value, cxstring name) {
+CxJsonValue *cx_json_obj_get(const CxJsonValue *value, cxstring name) {
     size_t index = json_find_objvalue(value, name);
     if (index >= value->value.object.values_size) {
         return &cx_json_value_nothing;
@@ -1159,14 +1186,14 @@
     }
 }
 
-CxJsonValue *cx_json_obj_remove_cxstr(CxJsonValue *value, cxstring name) {
+CxJsonValue *cx_json_obj_remove(CxJsonValue *value, cxstring name) {
     size_t index = json_find_objvalue(value, name);
     if (index >= value->value.object.values_size) {
         return NULL;
     } else {
         CxJsonObjValue kv = value->value.object.values[index];
         cx_strfree_a(value->allocator, &kv.name);
-        // TODO: replace with cx_array_remove()
+        // TODO: replace with cx_array_remove() / cx_array_remove_fast()
         value->value.object.values_size--;
         memmove(value->value.object.values + index, value->value.object.values + index + 1, (value->value.object.values_size - index) * sizeof(CxJsonObjValue));
         return kv.value;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ucx/kv_list.c	Sat Nov 08 23:06:11 2025 +0100
@@ -0,0 +1,705 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "cx/kv_list.h"
+#include "cx/hash_map.h"
+#include "cx/linked_list.h"
+
+#include <string.h>
+#include <assert.h>
+
+typedef struct cx_kv_list_s cx_kv_list;
+
+struct cx_kv_list_map_s {
+    struct cx_hash_map_s map_base;
+    /** Back-reference to the list. */
+    cx_kv_list *list;
+};
+
+struct cx_kv_list_s {
+    struct cx_linked_list_s list;
+    /** The lookup map - stores pointers to the nodes. */
+    struct cx_kv_list_map_s *map;
+    const cx_list_class *list_methods;
+    const cx_map_class *map_methods;
+    cx_destructor_func list_destr;
+    cx_destructor_func2 list_destr2;
+    void *list_destr_data;
+    cx_destructor_func map_destr;
+    cx_destructor_func2 map_destr2;
+    void *map_destr_data;
+};
+
+static void cx_kv_list_destructor_wrapper(void *list_ptr, void *elem) {
+    const cx_kv_list *kv_list = list_ptr;
+    // list destructor is already called with proper deref of the elem
+    if (kv_list->list_destr) {
+        kv_list->list_destr(elem);
+    }
+    if (kv_list->list_destr2) {
+        kv_list->list_destr2(kv_list->list_destr_data, elem);
+    }
+    if (kv_list->map_destr) {
+        kv_list->map_destr(elem);
+    }
+    if (kv_list->map_destr2) {
+        kv_list->map_destr2(kv_list->map_destr_data, elem);
+    }
+}
+
+static void cx_kv_list_update_destructors(cx_kv_list *list) {
+    // we copy the destructors to our custom fields and register
+    // an own destructor function which invokes all these
+    if (list->list.base.collection.simple_destructor != NULL) {
+        list->list_destr = list->list.base.collection.simple_destructor;
+        list->list.base.collection.simple_destructor = NULL;
+    }
+    if (list->list.base.collection.advanced_destructor != cx_kv_list_destructor_wrapper) {
+        list->list_destr2 = list->list.base.collection.advanced_destructor;
+        list->list_destr_data = list->list.base.collection.destructor_data;
+        list->list.base.collection.advanced_destructor = cx_kv_list_destructor_wrapper;
+        list->list.base.collection.destructor_data = list;
+    }
+    if (list->map->map_base.base.collection.simple_destructor != NULL) {
+        list->map_destr = list->map->map_base.base.collection.simple_destructor;
+        list->map->map_base.base.collection.simple_destructor = NULL;
+    }
+    if (list->map->map_base.base.collection.advanced_destructor != NULL) {
+        list->map_destr2 = list->map->map_base.base.collection.advanced_destructor;
+        list->map_destr_data = list->map->map_base.base.collection.destructor_data;
+        list->map->map_base.base.collection.advanced_destructor = NULL;
+        list->map->map_base.base.collection.destructor_data = NULL;
+    }
+}
+
+static CxHashKey *cx_kv_list_loc_key(cx_kv_list *list, void *node_data) {
+    return (CxHashKey*)((char*)node_data + list->list.base.collection.elem_size);
+}
+
+static void cx_kvl_deallocate(struct cx_list_s *list) {
+    cx_kv_list *kv_list = (cx_kv_list*)list;
+    // patch the destructors
+    cx_kv_list_update_destructors(kv_list);
+    kv_list->map_methods->deallocate(&kv_list->map->map_base.base);
+    // then free the list, now the destructors may be called
+    kv_list->list_methods->deallocate(list);
+}
+
+static void *cx_kvl_insert_element(
+        struct cx_list_s *list,
+        size_t index,
+        const void *data
+) {
+    cx_kv_list *kv_list = (cx_kv_list*)list;
+    return kv_list->list_methods->insert_element(list, index, data);
+}
+
+static size_t cx_kvl_insert_array(
+        struct cx_list_s *list,
+        size_t index,
+        const void *data,
+        size_t n
+) {
+    cx_kv_list *kv_list = (cx_kv_list*)list;
+    return kv_list->list_methods->insert_array(list, index, data, n);
+}
+
+static size_t cx_kvl_insert_sorted(
+        struct cx_list_s *list,
+        const void *sorted_data,
+        size_t n
+) {
+    cx_kv_list *kv_list = (cx_kv_list*)list;
+    return kv_list->list_methods->insert_sorted(list, sorted_data, n);
+}
+
+static size_t cx_kvl_insert_unique(
+        struct cx_list_s *list,
+        const void *sorted_data,
+        size_t n
+) {
+    cx_kv_list *kv_list = (cx_kv_list*)list;
+    return kv_list->list_methods->insert_unique(list, sorted_data, n);
+}
+
+static int cx_kvl_insert_iter(
+        struct cx_iterator_s *iter,
+        const void *elem,
+        int prepend
+) {
+    cx_kv_list *kv_list = iter->src_handle;
+    return kv_list->list_methods->insert_iter(iter, elem, prepend);
+}
+
+static size_t cx_kvl_remove(
+        struct cx_list_s *list,
+        size_t index,
+        size_t num,
+        void *targetbuf
+) {
+    cx_kv_list *kv_list = (cx_kv_list*)list;
+    // patch the destructors
+    // we also have to do that when targetbuf is NULL,
+    // because we do not want wrong destructors to be called when we remove keys from the map
+    cx_kv_list_update_destructors(kv_list);
+    // iterate through the elements first and remove their keys from the map
+    CxIterator iter = kv_list->list_methods->iterator(list, index, false);
+    for (size_t i = 0; i < num && cxIteratorValid(iter); i++) {
+        void *node_data = cxIteratorCurrent(iter);
+        CxHashKey *key = cx_kv_list_loc_key(kv_list, node_data);
+        // when the hash is zero, there is no key assigned to that element
+        if (key->hash != 0) {
+            kv_list->map_methods->remove(&kv_list->map->map_base.base, *key, NULL);
+        }
+        cxIteratorNext(iter);
+    }
+    return kv_list->list_methods->remove(list, index, num, targetbuf);
+}
+
+static void cx_kvl_clear(struct cx_list_s *list) {
+    cx_kv_list *kv_list = (cx_kv_list*)list;
+    // patch the destructors
+    cx_kv_list_update_destructors(kv_list);
+    // clear the list
+    kv_list->list_methods->clear(list);
+    // then clear the map
+    kv_list->map_methods->clear(&kv_list->map->map_base.base);
+}
+
+static int cx_kvl_swap(
+        struct cx_list_s *list,
+        size_t i,
+        size_t j
+) {
+    cx_kv_list *kv_list = (cx_kv_list*)list;
+    return kv_list->list_methods->swap(list, i, j);
+}
+
+static void *cx_kvl_at(
+        const struct cx_list_s *list,
+        size_t index
+) {
+    const cx_kv_list *kv_list = (const cx_kv_list*)list;
+    return kv_list->list_methods->at(list, index);
+}
+
+static size_t cx_kvl_find_remove(
+        struct cx_list_s *list,
+        const void *elem,
+        bool remove
+) {
+    cx_kv_list *kv_list = (cx_kv_list*)list;
+    // we do not use the original list methods,
+    // because that would need two passes for removal
+    // (the first to find the index, the second to get a pointer)
+    if (list->collection.size == 0) return 0;
+
+    size_t index;
+    cx_linked_list *ll = &kv_list->list;
+    char *node = cx_linked_list_find(
+            ll->begin,
+            ll->loc_next, ll->loc_data,
+            list->collection.cmpfunc, elem,
+            &index
+    );
+    if (node == NULL) {
+        return list->collection.size;
+    }
+    if (remove) {
+        cx_kv_list_update_destructors(kv_list);
+        cx_invoke_advanced_destructor(list, node + ll->loc_data);
+        cx_linked_list_remove(&ll->begin, &ll->end,
+                              ll->loc_prev, ll->loc_next, node);
+        CxHashKey *key = cx_kv_list_loc_key(kv_list, node + ll->loc_data);
+        if (key->hash != 0) {
+            kv_list->map_methods->remove(&kv_list->map->map_base.base, *key, NULL);
+        }
+        list->collection.size--;
+        cxFree(list->collection.allocator, node);
+    }
+    return index;
+}
+
+static void cx_kvl_sort(struct cx_list_s *list) {
+    cx_kv_list *kv_list = (cx_kv_list*)list;
+    kv_list->list_methods->sort(list);
+}
+
+static void cx_kvl_reverse(struct cx_list_s *list) {
+    cx_kv_list *kv_list = (cx_kv_list*)list;
+    kv_list->list_methods->reverse(list);
+}
+
+static void cx_kvl_list_iter_next(void *it) {
+    struct cx_iterator_s *iter = it;
+    if (iter->base.remove) {
+        // remove the assigned key from the map before calling the actual function
+        cx_kv_list *kv_list = iter->src_handle;
+        cx_kv_list_update_destructors(kv_list);
+        char *node = iter->elem_handle;
+        CxHashKey *key = cx_kv_list_loc_key(kv_list, node + kv_list->list.loc_data);
+        if (key->hash != 0) {
+            kv_list->map_methods->remove(&kv_list->map->map_base.base, *key, NULL);
+        }
+    }
+    // note that we do not clear the remove flag, because the next_impl will do that
+    iter->base.next_impl(it);
+}
+
+static struct cx_iterator_s cx_kvl_iterator(
+        const struct cx_list_s *list,
+        size_t index,
+        bool backward
+) {
+    const cx_kv_list *kv_list = (const cx_kv_list*)list;
+    struct cx_iterator_s iter = kv_list->list_methods->iterator(list, index, backward);
+    iter.base.next_impl = iter.base.next;
+    iter.base.next = cx_kvl_list_iter_next;
+    return iter;
+}
+
+static void cx_kvl_map_deallocate(struct cx_map_s *map) {
+    cx_kv_list *kv_list = ((struct cx_kv_list_map_s*)map)->list;
+    kv_list->map_methods->deallocate(map);
+    kv_list->list_methods->deallocate(&kv_list->list.base);
+}
+
+static void cx_kvl_map_clear(struct cx_map_s *map) {
+    cx_kv_list *kv_list = ((struct cx_kv_list_map_s*)map)->list;
+    cx_kv_list_update_destructors(kv_list);
+    kv_list->list_methods->clear(&kv_list->list.base);
+    kv_list->map_methods->clear(map);
+}
+
+static void *cx_kvl_map_put(CxMap *map, CxHashKey key, void *value) {
+    cx_kv_list *kv_list = ((struct cx_kv_list_map_s*)map)->list;
+    // if the hash has not yet been computed, do it now
+    if (key.hash == 0) {
+        cx_hash_murmur(&key);
+    }
+
+    // reserve memory in the map first
+    void **map_data = kv_list->map_methods->put(map, key, NULL);
+    if (map_data == NULL) return NULL; // LCOV_EXCL_LINE
+
+    // insert the data into the list (which most likely destroys the sorted property)
+    kv_list->list.base.collection.sorted = false;
+    void *node_data = kv_list->list_methods->insert_element(
+        &kv_list->list.base, kv_list->list.base.collection.size,
+        kv_list->list.base.collection.store_pointer ? &value : value);
+    if (node_data == NULL) { // LCOV_EXCL_START
+        // non-destructively remove the key again
+        kv_list->map_methods->remove(&kv_list->map->map_base.base, key, &map_data);
+        return NULL;
+    } // LCOV_EXCL_STOP
+
+    // write the node pointer to the map entry
+    *map_data = node_data;
+
+    // copy the key to the node data
+    CxHashKey *key_ptr = cx_kv_list_loc_key(kv_list, node_data);
+    *key_ptr = key;
+
+    // we must return node_data here and not map_data,
+    // because the node_data is the actual element of this collection
+    return node_data;
+}
+
+void *cx_kvl_map_get(const CxMap *map, CxHashKey key) {
+    cx_kv_list *kv_list = ((struct cx_kv_list_map_s*)map)->list;
+    void *node_data = kv_list->map_methods->get(map, key);
+    if (node_data == NULL) return NULL; // LCOV_EXCL_LINE
+    // return the node data
+    return kv_list->list.base.collection.store_pointer ? *(void**)node_data : node_data;
+}
+
+int cx_kvl_map_remove(CxMap *map, CxHashKey key, void *targetbuf) {
+    cx_kv_list *kv_list = ((struct cx_kv_list_map_s*)map)->list;
+
+    void *node_data;
+    if (kv_list->map_methods->remove(map, key, &node_data)) {
+        return 1;
+    }
+    // we cannot just call a list method (because we don't have the index)
+    // and tbh. we also don't want to (because it's not performant when we
+    // can have the node ptr directly instead)
+    // therefore, we re-implement the logic ourselves
+
+    // check if the outside caller want's us to return or to destroy the element
+    if (targetbuf == NULL) {
+        // patch the destructors and invoke them through the wrapper
+        cx_kv_list_update_destructors(kv_list);
+        cx_invoke_advanced_destructor(&kv_list->list.base, node_data);
+    } else {
+        // copy the element to the target buffer
+        memcpy(targetbuf, node_data, kv_list->list.base.collection.elem_size);
+    }
+
+    // calculate the address of the node
+    void *node_ptr = (char*)node_data - kv_list->list.loc_data;
+
+    // unlink the node
+    cx_linked_list_remove(
+        &kv_list->list.begin,
+        &kv_list->list.end,
+        kv_list->list.loc_prev,
+        kv_list->list.loc_next,
+        node_ptr
+    );
+
+    // decrement the list's size
+    kv_list->list.base.collection.size--;
+
+    // deallocate the node
+    cxFree(kv_list->list.base.collection.allocator, node_ptr);
+
+    return 0;
+}
+
+static void *cx_kvl_iter_current_entry(const void *it) {
+    const CxMapIterator *iter = it;
+    return (void*)&iter->entry;
+}
+
+static void *cx_kvl_iter_current_key(const void *it) {
+    const CxMapEntry *entry = cx_kvl_iter_current_entry(it);
+    return (void*)entry->key;
+}
+
+static void *cx_kvl_iter_current_value(const void *it) {
+    const CxMapEntry *entry = cx_kvl_iter_current_entry(it);
+    return entry->value;
+}
+
+static void cx_kvl_iter_next(void *it) {
+    CxMapIterator *iter = it;
+    cx_kv_list *kv_list = ((struct cx_kv_list_map_s*)iter->map)->list;
+
+    // find the next list entry that has a key assigned
+    CxHashKey *key = NULL;
+    char *next = iter->elem;
+    while (true) {
+        next = *(char**)(next + kv_list->list.loc_next);
+        if (next == NULL) break;
+        key = cx_kv_list_loc_key(kv_list, next + kv_list->list.loc_data);
+        if (key->hash != 0) break;
+    }
+
+    // remove previous element if requested
+    if (iter->base.remove) {
+        iter->base.remove = false;
+        cx_kv_list_update_destructors(kv_list);
+        char *elem = iter->elem;
+        char *elem_data = elem + kv_list->list.loc_data;
+        CxHashKey *elem_key = cx_kv_list_loc_key(kv_list, elem_data);
+        // key is guaranteed to exist because iterator only iterates over elems with a key
+        kv_list->map_methods->remove(&kv_list->map->map_base.base, *elem_key, NULL);
+        cx_invoke_advanced_destructor(&kv_list->list.base, elem_data);
+        cx_linked_list_remove(
+            &kv_list->list.begin,
+            &kv_list->list.end,
+            kv_list->list.loc_prev,
+            kv_list->list.loc_next,
+            elem
+        );
+        cxFree(kv_list->list.base.collection.allocator, elem);
+        kv_list->list.base.collection.size--;
+        iter->index--;
+        iter->elem_count--;
+    }
+
+    // advance to the next element, if any
+    if (next == NULL) {
+        iter->index = kv_list->list.base.collection.size;
+        iter->elem = NULL;
+        iter->entry = (CxMapEntry){NULL, NULL};
+        return;
+    }
+    iter->index++;
+    iter->elem = next;
+    iter->entry.key = key;
+    if (kv_list->list.base.collection.store_pointer) {
+        iter->entry.value = *(void**)(next + kv_list->list.loc_data);
+    } else {
+        iter->entry.value = (void*)(next + kv_list->list.loc_data);
+    }
+}
+
+static bool cx_kvl_iter_valid(const void *it) {
+    const CxMapIterator *iter = it;
+    return iter->elem != NULL;
+}
+
+CxMapIterator cx_kvl_map_iterator(const CxMap *map, enum cx_map_iterator_type type) {
+    CxMapIterator iter = {0};
+
+    iter.type = type;
+    iter.map = (CxMap*)map;
+    // although we iterate over the list, we only report that many elements that have a key in the map
+    iter.elem_count = map->collection.size;
+
+    switch (type) {
+        case CX_MAP_ITERATOR_PAIRS:
+            iter.elem_size = sizeof(CxMapEntry);
+            iter.base.current = cx_kvl_iter_current_entry;
+            break;
+        case CX_MAP_ITERATOR_KEYS:
+            iter.elem_size = sizeof(CxHashKey);
+            iter.base.current = cx_kvl_iter_current_key;
+            break;
+        case CX_MAP_ITERATOR_VALUES:
+            iter.elem_size = map->collection.elem_size;
+            iter.base.current = cx_kvl_iter_current_value;
+            break;
+        default:
+            assert(false); // LCOV_EXCL_LINE
+    }
+
+    iter.base.allow_remove = true;
+    iter.base.next = cx_kvl_iter_next;
+    iter.base.valid = cx_kvl_iter_valid;
+
+    // find the first list entry that has a key assigned
+    cx_kv_list *kv_list = ((struct cx_kv_list_map_s*)map)->list;
+    CxHashKey *key = NULL;
+    char *next = kv_list->list.begin;
+    while (next != NULL) {
+        key = cx_kv_list_loc_key(kv_list, next + kv_list->list.loc_data);
+        if (key->hash != 0) break;
+        next = *(char**)(next + kv_list->list.loc_next);
+    }
+    if (next == NULL) {
+        iter.elem = NULL;
+        iter.entry = (CxMapEntry){NULL, NULL};
+    } else {
+        iter.elem = next;
+        iter.entry.key = key;
+        if (kv_list->list.base.collection.store_pointer) {
+            iter.entry.value = *(void**)(next + kv_list->list.loc_data);
+        } else {
+            iter.entry.value = (void*)(next + kv_list->list.loc_data);
+        }
+    }
+
+    return iter;
+}
+
+static cx_list_class cx_kv_list_class = {
+    cx_kvl_deallocate,
+    cx_kvl_insert_element,
+    cx_kvl_insert_array,
+    cx_kvl_insert_sorted,
+    cx_kvl_insert_unique,
+    cx_kvl_insert_iter,
+    cx_kvl_remove,
+    cx_kvl_clear,
+    cx_kvl_swap,
+    cx_kvl_at,
+    cx_kvl_find_remove,
+    cx_kvl_sort,
+    NULL,
+    cx_kvl_reverse,
+    cx_kvl_iterator,
+};
+
+static cx_map_class cx_kv_map_class = {
+    cx_kvl_map_deallocate,
+    cx_kvl_map_clear,
+    cx_kvl_map_put,
+    cx_kvl_map_get,
+    cx_kvl_map_remove,
+    cx_kvl_map_iterator,
+};
+
+CxList *cxKvListCreate(
+        const CxAllocator *allocator,
+        cx_compare_func comparator,
+        size_t elem_size
+) {
+    if (allocator == NULL) {
+        allocator = cxDefaultAllocator;
+    }
+
+    // create a normal linked list and a normal hash map, first
+    CxList *list = cxLinkedListCreate(allocator, comparator, elem_size);
+    if (list == NULL) return NULL; // LCOV_EXCL_LINE
+    cx_linked_list *ll = (cx_linked_list*)list;
+    ll->extra_data_len = sizeof(CxHashKey);
+    CxMap *map = cxHashMapCreate(allocator, CX_STORE_POINTERS, 0);
+    if (map == NULL) { // LCOV_EXCL_START
+        cxListFree(list);
+        return NULL;
+    } // LCOV_EXCL_STOP
+
+    // patch the kv-list class with the compare function of the linked list
+    // this allows cxListCompare() to optimize comparisons between linked lists and kv-list
+    cx_kv_list_class.compare = list->cl->compare;
+
+    // reallocate the map to add memory for the list back-reference
+    struct cx_kv_list_map_s *kv_map = cxRealloc(allocator, map, sizeof(struct cx_kv_list_map_s));
+    
+    // reallocate the list to add memory for storing the metadata
+    cx_kv_list *kv_list = cxRealloc(allocator, list, sizeof(struct cx_kv_list_s));
+
+    // if any of the reallocations failed, we bail out
+    if (kv_map != NULL && kv_list != NULL) {
+        map = (CxMap*) kv_map;
+        list = (CxList*) kv_list;
+    } else { // LCOV_EXCL_START
+        cxListFree(list);
+        cxMapFree(map);
+        return NULL;
+    } // LCOV_EXCL_STOP
+
+    // zero the custom destructor information
+    memset((char*)kv_list + offsetof(cx_kv_list, list_destr), 0, sizeof(void*)*6);
+
+    // combine the list and the map aspect
+    kv_list->map = kv_map;
+    kv_map->list = kv_list;
+
+    // remember the base methods and override them
+    kv_list->map_methods = map->cl;
+    map->cl = &cx_kv_map_class;
+    if (list->climpl == NULL) {
+        kv_list->list_methods = list->cl;
+        list->cl = &cx_kv_list_class;
+    } else {
+        kv_list->list_methods = list->climpl;
+        list->climpl = &cx_kv_list_class;
+    }
+
+    return list;
+}
+
+CxMap *cxKvListCreateAsMap(
+        const CxAllocator *allocator,
+        cx_compare_func comparator,
+        size_t elem_size
+) {
+    CxList *list = cxKvListCreate(allocator, comparator, elem_size);
+    return list == NULL ? NULL : cxKvListAsMap(list);
+}
+
+CxList *cxKvListAsList(CxMap *map) {
+    return &((struct cx_kv_list_map_s*)map)->list->list.base;
+}
+
+CxMap *cxKvListAsMap(CxList *list) {
+    return &((cx_kv_list*)list)->map->map_base.base;
+}
+
+int cx_kv_list_set_key(CxList *list, size_t index, CxHashKey key) {
+    cx_kv_list *kv_list = (cx_kv_list*)list;
+    void *node_data = kv_list->list_methods->at(list, index);
+    if (node_data == NULL) {
+        return 1;
+    }
+    // if the hash has not yet been computed, do it now
+    if (key.hash == 0) {
+        cx_hash_murmur(&key);
+    }
+
+    // check if the key is already assigned
+    void *existing = kv_list->map_methods->get(&kv_list->map->map_base.base, key);
+    if (existing == node_data) {
+        return 0; // nothing to do
+    }
+    if (existing != NULL) {
+        // the key is already assigned to another node, we disallow re-assignment
+        return 1;
+    }
+
+    // add the key to the map;
+    if (NULL == kv_list->map_methods->put(&kv_list->map->map_base.base, key, node_data)) {
+        return 1; // LCOV_EXCL_LINE
+    }
+
+    // write the key to the list's node
+    CxHashKey *loc_key = cx_kv_list_loc_key(kv_list, node_data);
+    *loc_key = key;
+
+    return 0;
+}
+
+int cxKvListRemoveKey(CxList *list, size_t index) {
+    cx_kv_list *kv_list = (cx_kv_list*)list;
+    void *node_data = kv_list->list_methods->at(list, index);
+    if (node_data == NULL) {
+        return 1;
+    }
+    CxHashKey *loc_key = cx_kv_list_loc_key(kv_list, node_data);
+    if (loc_key->hash == 0) {
+        return 0;
+    }
+    kv_list->map_methods->remove(&kv_list->map->map_base.base, *loc_key, NULL);
+    // also zero the memory in the list node,
+    // but don't free the key data (that was done by the map remove)
+    memset(loc_key, 0, sizeof(CxHashKey));
+    return 0;
+}
+
+const CxHashKey *cxKvListGetKey(CxList *list, size_t index) {
+    cx_kv_list *kv_list = (cx_kv_list*)list;
+    void *node_data = kv_list->list_methods->at(list, index);
+    if (node_data == NULL) {
+        return NULL;
+    }
+    CxHashKey *key = cx_kv_list_loc_key(kv_list, node_data);
+    if (key->hash == 0) {
+        return NULL;
+    }
+    return key;
+}
+
+int cx_kv_list_insert(CxList *list, size_t index, CxHashKey key, void *value) {
+    // assume we are losing the sorted property
+    list->collection.sorted = false;
+
+    cx_kv_list *kv_list = (cx_kv_list*)list;
+
+    // reserve memory in the map
+    void **map_data = kv_list->map_methods->put(&kv_list->map->map_base.base, key, NULL);
+    if (map_data == NULL) return 1; // LCOV_EXCL_LINE
+
+    // insert the node
+    void *node_data = kv_list->list_methods->insert_element(&kv_list->list.base, index,
+        kv_list->list.base.collection.store_pointer ? &value : value);
+    if (node_data == NULL) { // LCOV_EXCL_START
+        // non-destructively remove the key again
+        kv_list->map_methods->remove(&kv_list->map->map_base.base, key, &map_data);
+        return 1;
+    } // LCOV_EXCL_STOP
+    *map_data = node_data;
+
+    // write the key to the node
+    CxHashKey *loc_key = cx_kv_list_loc_key(kv_list, node_data);
+    *loc_key = key;
+
+    return 0;
+}
--- a/ucx/linked_list.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/linked_list.c	Sat Nov 08 23:06:11 2025 +0100
@@ -244,6 +244,147 @@
             begin, end, loc_prev, loc_next, new_node, cmp_func);
 }
 
+static void *cx_linked_list_insert_sorted_chain_impl(
+        void **begin,
+        void **end,
+        ptrdiff_t loc_prev,
+        ptrdiff_t loc_next,
+        void *insert_begin,
+        cx_compare_func cmp_func,
+        bool allow_duplicates
+) {
+    assert(begin != NULL);
+    assert(loc_next >= 0);
+    assert(insert_begin != NULL);
+
+    // strategy: build completely new chains from scratch
+    void *source_original = *begin;
+    void *source_argument = insert_begin;
+    void *new_begin = NULL;
+    void *new_end = NULL;
+    void *dup_begin = NULL;
+    void *dup_end = NULL;
+
+    // determine the new start
+    {
+        int d = source_original ==  NULL ? 1 : cmp_func(source_original, source_argument);
+        if (d <= 0) {
+            // the new chain starts with the original chain
+            new_begin = new_end = source_original;
+            source_original = ll_next(source_original);
+            if (d == 0) {
+                if (allow_duplicates) {
+                    // duplicate allowed, append it to the chain
+                    cx_linked_list_link(new_end, source_argument, loc_prev, loc_next);
+                    new_end = source_argument;
+                } else {
+                    // duplicate is not allowed, start a duplicate chain with the argument
+                    dup_begin = dup_end = source_argument;
+                }
+                source_argument = ll_next(source_argument);
+            }
+        } else {
+            // input is smaller, or there is no original chain;
+            // start the new chain with the source argument
+            new_begin = new_end = source_argument;
+            source_argument = ll_next(source_argument);
+        }
+    }
+
+    // now successively compare the elements and add them to the correct chains
+    while (source_original != NULL && source_argument != NULL) {
+        int d = cmp_func(source_original, source_argument);
+        if (d <= 0) {
+            // the original is not larger, add it to the chain
+            cx_linked_list_link(new_end, source_original, loc_prev, loc_next);
+            new_end = source_original;
+            source_original = ll_next(source_original);
+            if (d == 0) {
+                if (allow_duplicates) {
+                    // duplicate allowed, append it to the chain
+                    cx_linked_list_link(new_end, source_argument, loc_prev, loc_next);
+                    new_end = source_argument;
+                } else {
+                    // duplicate is not allowed, append it to the duplicate chain
+                    if (dup_end == NULL) {
+                        dup_begin = dup_end = source_argument;
+                    } else {
+                        cx_linked_list_link(dup_end, source_argument, loc_prev, loc_next);
+                        dup_end = source_argument;
+                    }
+                }
+                source_argument = ll_next(source_argument);
+            }
+        } else {
+            // the original is larger, append the source argument to the chain
+            // check if we must discard the source argument as duplicate
+            if (!allow_duplicates && cmp_func(new_end, source_argument) == 0) {
+                if (dup_end == NULL) {
+                    dup_begin = dup_end = source_argument;
+                } else {
+                    cx_linked_list_link(dup_end, source_argument, loc_prev, loc_next);
+                    dup_end = source_argument;
+                }
+            } else {
+                // no duplicate or duplicates allowed
+                cx_linked_list_link(new_end, source_argument, loc_prev, loc_next);
+                new_end = source_argument;
+            }
+            source_argument = ll_next(source_argument);
+        }
+    }
+
+    if (source_original != NULL) {
+        // something is left from the original chain, append it
+        cx_linked_list_link(new_end, source_original, loc_prev, loc_next);
+        new_end = cx_linked_list_last(source_original, loc_next);
+    } else if (source_argument != NULL) {
+        // something is left from the input chain;
+        // when we allow duplicates, append it
+        if (allow_duplicates) {
+            cx_linked_list_link(new_end, source_argument, loc_prev, loc_next);
+            new_end = cx_linked_list_last(source_argument, loc_next);
+        } else {
+            // otherwise we must check one-by-one
+            while (source_argument != NULL) {
+                if (cmp_func(new_end, source_argument) == 0) {
+                    if (dup_end == NULL) {
+                        dup_begin = dup_end = source_argument;
+                    } else {
+                        cx_linked_list_link(dup_end, source_argument, loc_prev, loc_next);
+                        dup_end = source_argument;
+                    }
+                } else {
+                    cx_linked_list_link(new_end, source_argument, loc_prev, loc_next);
+                    new_end = source_argument;
+                }
+                source_argument = ll_next(source_argument);
+            }
+        }
+    }
+
+    // null the next pointers at the end of the chain
+    ll_next(new_end) = NULL;
+    if (dup_end != NULL) {
+        ll_next(dup_end) = NULL;
+    }
+
+    // null the optional prev pointers
+    if (loc_prev >= 0) {
+        ll_prev(new_begin) = NULL;
+        if (dup_begin != NULL) {
+            ll_prev(dup_begin) = NULL;
+        }
+    }
+
+    // output
+    *begin = new_begin;
+    if (end != NULL) {
+        *end = new_end;
+    }
+    return dup_begin;
+}
+
 void cx_linked_list_insert_sorted_chain(
         void **begin,
         void **end,
@@ -252,72 +393,35 @@
         void *insert_begin,
         cx_compare_func cmp_func
 ) {
-    assert(begin != NULL);
-    assert(loc_next >= 0);
-    assert(insert_begin != NULL);
-
-    // track currently observed nodes
-    void *dest_prev = NULL;
-    void *dest = *begin;
-    void *src = insert_begin;
-
-    // special case: list is empty
-    if (dest == NULL) {
-        *begin = src;
-        if (end != NULL) {
-            *end = cx_linked_list_last(src, loc_next);
-        }
-        return;
-    }
-
-    // search the list for insertion points
-    while (dest != NULL && src != NULL) {
-        // compare current list node with source node
-        // if less or equal, skip
-        if (cmp_func(dest, src) <= 0) {
-            dest_prev = dest;
-            dest = ll_next(dest);
-            continue;
-        }
+    cx_linked_list_insert_sorted_chain_impl(
+            begin, end, loc_prev, loc_next,
+            insert_begin, cmp_func, true);
+}
 
-        // determine chain of elements that can be inserted
-        void *end_of_chain = src;
-        void *next_in_chain = ll_next(src);
-        while (next_in_chain != NULL) {
-            // once we become larger than the list elem, break
-            if (cmp_func(dest, next_in_chain) <= 0) {
-                break;
-            }
-            // otherwise, we can insert one more
-            end_of_chain = next_in_chain;
-            next_in_chain = ll_next(next_in_chain);
-        }
+int cx_linked_list_insert_unique(
+        void **begin,
+        void **end,
+        ptrdiff_t loc_prev,
+        ptrdiff_t loc_next,
+        void *new_node,
+        cx_compare_func cmp_func
+) {
+    assert(ll_next(new_node) == NULL);
+    return NULL != cx_linked_list_insert_unique_chain(
+            begin, end, loc_prev, loc_next, new_node, cmp_func);
+}
 
-        // insert the elements
-        if (dest_prev == NULL) {
-            // new begin
-            *begin = src;
-        } else {
-            cx_linked_list_link(dest_prev, src, loc_prev, loc_next);
-        }
-        cx_linked_list_link(end_of_chain, dest, loc_prev, loc_next);
-
-        // continue with next
-        src = next_in_chain;
-        dest_prev = dest;
-        dest = ll_next(dest);
-    }
-
-    // insert remaining items
-    if (src != NULL) {
-        cx_linked_list_link(dest_prev, src, loc_prev, loc_next);
-    }
-
-    // determine new end of list, if requested
-    if (end != NULL) {
-        *end = cx_linked_list_last(
-                dest != NULL ? dest : dest_prev, loc_next);
-    }
+void *cx_linked_list_insert_unique_chain(
+        void **begin,
+        void **end,
+        ptrdiff_t loc_prev,
+        ptrdiff_t loc_next,
+        void *insert_begin,
+        cx_compare_func cmp_func
+) {
+    return cx_linked_list_insert_sorted_chain_impl(
+            begin, end, loc_prev, loc_next,
+            insert_begin, cmp_func, false);
 }
 
 size_t cx_linked_list_remove_chain(
@@ -370,6 +474,16 @@
     return removed;
 }
 
+void cx_linked_list_remove(
+        void **begin,
+        void **end,
+        ptrdiff_t loc_prev,
+        ptrdiff_t loc_next,
+        void *node
+) {
+    cx_linked_list_remove_chain(begin, end, loc_prev, loc_next, node, 1);
+}
+
 size_t cx_linked_list_size(
         const void *node,
         ptrdiff_t loc_next
@@ -564,65 +678,46 @@
 
 // HIGH LEVEL LINKED LIST IMPLEMENTATION
 
-typedef struct cx_linked_list_node cx_linked_list_node;
-// IMPORTANT: the layout must stay exactly like this, because kv_list.c uses that!
-struct cx_linked_list_node {
-    cx_linked_list_node *prev;
-    cx_linked_list_node *next;
-    char payload[];
-};
-
-#define CX_LL_LOC_PREV offsetof(cx_linked_list_node, prev)
-#define CX_LL_LOC_NEXT offsetof(cx_linked_list_node, next)
-#define CX_LL_LOC_DATA offsetof(cx_linked_list_node, payload)
-
-typedef struct {
-    struct cx_list_s base;
-    cx_linked_list_node *begin;
-    cx_linked_list_node *end;
-} cx_linked_list;
-
-static cx_linked_list_node *cx_ll_node_at(
+static void *cx_ll_node_at(
         const cx_linked_list *list,
         size_t index
 ) {
     if (index >= list->base.collection.size) {
         return NULL;
     } else if (index > list->base.collection.size / 2) {
-        return cx_linked_list_at(list->end, list->base.collection.size - 1, CX_LL_LOC_PREV, index);
+        return cx_linked_list_at(list->end, list->base.collection.size - 1, list->loc_prev, index);
     } else {
-        return cx_linked_list_at(list->begin, 0, CX_LL_LOC_NEXT, index);
+        return cx_linked_list_at(list->begin, 0, list->loc_next, index);
     }
 }
 
-static cx_linked_list_node *cx_ll_malloc_node(const struct cx_list_s *list) {
-    return cxMalloc(list->collection.allocator,
-                    sizeof(cx_linked_list_node) + list->collection.elem_size);
+static void *cx_ll_malloc_node(const cx_linked_list *list) {
+    return cxZalloc(list->base.collection.allocator,
+                    list->loc_data + list->base.collection.elem_size + list->extra_data_len);
 }
 
 static int cx_ll_insert_at(
         struct cx_list_s *list,
-        cx_linked_list_node *node,
+        void *node,
         const void *elem
 ) {
+    cx_linked_list *ll = (cx_linked_list *) list;
 
     // create the new new_node
-    cx_linked_list_node *new_node = cx_ll_malloc_node(list);
+    void *new_node = cx_ll_malloc_node(ll);
 
     // sortir if failed
     if (new_node == NULL) return 1;
 
-    // initialize new new_node
-    new_node->prev = new_node->next = NULL;
+    // copy the data
     if (elem != NULL) {
-        memcpy(new_node->payload, elem, list->collection.elem_size);
+        memcpy((char*)new_node + ll->loc_data, elem, list->collection.elem_size);
     }
 
     // insert
-    cx_linked_list *ll = (cx_linked_list *) list;
     cx_linked_list_insert_chain(
-            (void **) &ll->begin, (void **) &ll->end,
-            CX_LL_LOC_PREV, CX_LL_LOC_NEXT,
+            &ll->begin, &ll->end,
+            ll->loc_prev, ll->loc_next,
             node, new_node, new_node
     );
 
@@ -641,7 +736,7 @@
     if (index > list->collection.size || n == 0) return 0;
 
     // find position efficiently
-    cx_linked_list_node *node = index == 0 ? NULL : cx_ll_node_at((cx_linked_list *) list, index - 1);
+    void *node = index == 0 ? NULL : cx_ll_node_at((cx_linked_list *) list, index - 1);
 
     // perform first insert
     if (0 != cx_ll_insert_at(list, node, array)) return 1;
@@ -650,14 +745,17 @@
     if (n == 1) return 1;
 
     // we now know exactly where we are
-    node = node == NULL ? ((cx_linked_list *) list)->begin : node->next;
+    cx_linked_list *ll = (cx_linked_list *) list;
+    node = node == NULL ? ((cx_linked_list *) list)->begin : CX_LL_PTR(node, ll->loc_next);
 
     // we can add the remaining nodes and immediately advance to the inserted node
     const char *source = array;
     for (size_t i = 1; i < n; i++) {
-        source += list->collection.elem_size;
+        if (source != NULL) {
+            source += list->collection.elem_size;
+        }
         if (0 != cx_ll_insert_at(list, node, source)) return i;
-        node = node->next;
+        node = CX_LL_PTR(node, ll->loc_next);
     }
     return n;
 }
@@ -671,25 +769,95 @@
     if (index > list->collection.size) return NULL;
 
     // find position efficiently
-    cx_linked_list_node *node = index == 0 ? NULL : cx_ll_node_at((cx_linked_list *) list, index - 1);
+    void *node = index == 0 ? NULL : cx_ll_node_at((cx_linked_list *) list, index - 1);
 
     // perform first insert
     if (cx_ll_insert_at(list, node, element)) return NULL;
 
     // return a pointer to the data of the inserted node
+    cx_linked_list *ll = (cx_linked_list *) list;
     if (node == NULL) {
-        return ((cx_linked_list *) list)->begin->payload;
+        return (char*)(ll->begin) + ll->loc_data;
     } else {
-        return node->next->payload;
+        char *next = CX_LL_PTR(node, ll->loc_next);
+        return next + ll->loc_data;
     }
 }
 
 static _Thread_local cx_compare_func cx_ll_insert_sorted_cmp_func;
+static _Thread_local off_t cx_ll_insert_sorted_loc_data;
 
 static int cx_ll_insert_sorted_cmp_helper(const void *l, const void *r) {
-    const cx_linked_list_node *left = l;
-    const cx_linked_list_node *right = r;
-    return cx_ll_insert_sorted_cmp_func(left->payload, right->payload);
+    const char *left = (const char*)l + cx_ll_insert_sorted_loc_data;
+    const char *right = (const char*)r + cx_ll_insert_sorted_loc_data;
+    return cx_ll_insert_sorted_cmp_func(left, right);
+}
+
+static size_t cx_ll_insert_sorted_impl(
+        struct cx_list_s *list,
+        const void *array,
+        size_t n,
+        bool allow_duplicates
+) {
+    cx_linked_list *ll = (cx_linked_list *) list;
+
+    // special case
+    if (n == 0) return 0;
+
+    // create a new chain of nodes
+    void *chain = cx_ll_malloc_node(ll);
+    if (chain == NULL) return 0;
+
+    memcpy((char*)chain + ll->loc_data, array, list->collection.elem_size);
+
+    // add all elements from the array to that chain
+    void *prev = chain;
+    const char *src = array;
+    size_t inserted = 1;
+    for (; inserted < n; inserted++) {
+        void *next = cx_ll_malloc_node(ll);
+        if (next == NULL) break;
+        src += list->collection.elem_size;
+        memcpy((char*)next + ll->loc_data, src, list->collection.elem_size);
+        CX_LL_PTR(prev, ll->loc_next) = next;
+        CX_LL_PTR(next, ll->loc_prev) = prev;
+        prev = next;
+    }
+    CX_LL_PTR(prev, ll->loc_next) = NULL;
+
+    // invoke the low level function
+    cx_ll_insert_sorted_cmp_func = list->collection.cmpfunc;
+    cx_ll_insert_sorted_loc_data = ll->loc_data;
+    if (allow_duplicates) {
+        cx_linked_list_insert_sorted_chain(
+                &ll->begin,
+                &ll->end,
+                ll->loc_prev,
+                ll->loc_next,
+                chain,
+                cx_ll_insert_sorted_cmp_helper
+        );
+        list->collection.size += inserted;
+    } else {
+        void *duplicates = cx_linked_list_insert_unique_chain(
+                &ll->begin,
+                &ll->end,
+                ll->loc_prev,
+                ll->loc_next,
+                chain,
+                cx_ll_insert_sorted_cmp_helper
+        );
+        list->collection.size += inserted;
+        // free the nodes that did not make it into the list
+        while (duplicates != NULL) {
+            void *next = CX_LL_PTR(duplicates, ll->loc_next);
+            cxFree(list->collection.allocator, duplicates);
+            duplicates = next;
+            list->collection.size--;
+        }
+    }
+
+    return inserted;
 }
 
 static size_t cx_ll_insert_sorted(
@@ -697,48 +865,15 @@
         const void *array,
         size_t n
 ) {
-    // special case
-    if (n == 0) return 0;
-
-    // create a new chain of nodes
-    cx_linked_list_node *chain = cx_ll_malloc_node(list);
-    if (chain == NULL) return 0;
-
-    memcpy(chain->payload, array, list->collection.elem_size);
-    chain->prev = NULL;
-    chain->next = NULL;
+    return cx_ll_insert_sorted_impl(list, array, n, true);
+}
 
-    // add all elements from the array to that chain
-    cx_linked_list_node *prev = chain;
-    const char *src = array;
-    size_t inserted = 1;
-    for (; inserted < n; inserted++) {
-        cx_linked_list_node *next = cx_ll_malloc_node(list);
-        if (next == NULL) break;
-        src += list->collection.elem_size;
-        memcpy(next->payload, src, list->collection.elem_size);
-        prev->next = next;
-        next->prev = prev;
-        prev = next;
-    }
-    prev->next = NULL;
-
-    // invoke the low level function
-    cx_linked_list *ll = (cx_linked_list *) list;
-    cx_ll_insert_sorted_cmp_func = list->collection.cmpfunc;
-    cx_linked_list_insert_sorted_chain(
-            (void **) &ll->begin,
-            (void **) &ll->end,
-            CX_LL_LOC_PREV,
-            CX_LL_LOC_NEXT,
-            chain,
-            cx_ll_insert_sorted_cmp_helper
-    );
-
-    // adjust the list metadata
-    list->collection.size += inserted;
-
-    return inserted;
+static size_t cx_ll_insert_unique(
+        struct cx_list_s *list,
+        const void *array,
+        size_t n
+) {
+    return cx_ll_insert_sorted_impl(list, array, n, false);
 }
 
 static size_t cx_ll_remove(
@@ -748,7 +883,7 @@
         void *targetbuf
 ) {
     cx_linked_list *ll = (cx_linked_list *) list;
-    cx_linked_list_node *node = cx_ll_node_at(ll, index);
+    void *node = cx_ll_node_at(ll, index);
 
     // out-of-bounds check
     if (node == NULL) return 0;
@@ -757,8 +892,8 @@
     size_t removed = cx_linked_list_remove_chain(
             (void **) &ll->begin,
             (void **) &ll->end,
-            CX_LL_LOC_PREV,
-            CX_LL_LOC_NEXT,
+            ll->loc_prev,
+            ll->loc_next,
             node,
             num
     );
@@ -768,28 +903,28 @@
 
     // copy or destroy the removed chain
     if (targetbuf == NULL) {
-        cx_linked_list_node *n = node;
+        char *n = node;
         for (size_t i = 0; i < removed; i++) {
             // element destruction
-            cx_invoke_destructor(list, n->payload);
+            cx_invoke_destructor(list, n + ll->loc_data);
 
             // free the node and advance
-            void *next = n->next;
+            void *next = CX_LL_PTR(n, ll->loc_next);
             cxFree(list->collection.allocator, n);
             n = next;
         }
     } else {
         char *dest = targetbuf;
-        cx_linked_list_node *n = node;
+        char *n = node;
         for (size_t i = 0; i < removed; i++) {
             // copy payload
-            memcpy(dest, n->payload, list->collection.elem_size);
+            memcpy(dest, n + ll->loc_data, list->collection.elem_size);
 
             // advance target buffer
             dest += list->collection.elem_size;
 
             // free the node and advance
-            void *next = n->next;
+            void *next = CX_LL_PTR(n, ll->loc_next);
             cxFree(list->collection.allocator, n);
             n = next;
         }
@@ -802,10 +937,10 @@
     if (list->collection.size == 0) return;
 
     cx_linked_list *ll = (cx_linked_list *) list;
-    cx_linked_list_node *node = ll->begin;
+    char *node = ll->begin;
     while (node != NULL) {
-        cx_invoke_destructor(list, node->payload);
-        cx_linked_list_node *next = node->next;
+        cx_invoke_destructor(list, node + ll->loc_data);
+        void *next = CX_LL_PTR(node, ll->loc_next);
         cxFree(list->collection.allocator, node);
         node = next;
     }
@@ -832,14 +967,14 @@
         left = j;
         right = i;
     }
-    cx_linked_list_node *nleft = NULL, *nright = NULL;
+    void *nleft = NULL, *nright = NULL;
     if (left < mid && right < mid) {
         // case 1: both items left from mid
         nleft = cx_ll_node_at(ll, left);
         assert(nleft != NULL);
         nright = nleft;
         for (size_t c = left; c < right; c++) {
-            nright = nright->next;
+            nright = CX_LL_PTR(nright, ll->loc_next);
         }
     } else if (left >= mid && right >= mid) {
         // case 2: both items right from mid
@@ -847,7 +982,7 @@
         assert(nright != NULL);
         nleft = nright;
         for (size_t c = right; c > left; c--) {
-            nleft = nleft->prev;
+            nleft = CX_LL_PTR(nleft, ll->loc_prev);
         }
     } else {
         // case 3: one item left, one item right
@@ -873,12 +1008,12 @@
             if (closest == left) {
                 nright = nleft;
                 for (size_t c = left; c < right; c++) {
-                    nright = nright->next;
+                    nright = CX_LL_PTR(nright, ll->loc_next);
                 }
             } else {
                 nleft = nright;
                 for (size_t c = right; c > left; c--) {
-                    nleft = nleft->prev;
+                    nleft = CX_LL_PTR(nleft, ll->loc_prev);
                 }
             }
         } else {
@@ -891,33 +1026,33 @@
         }
     }
 
-    cx_linked_list_node *prev = nleft->prev;
-    cx_linked_list_node *next = nright->next;
-    cx_linked_list_node *midstart = nleft->next;
-    cx_linked_list_node *midend = nright->prev;
+    void *prev = CX_LL_PTR(nleft, ll->loc_prev);
+    void *next = CX_LL_PTR(nright, ll->loc_next);
+    void *midstart = CX_LL_PTR(nleft, ll->loc_next);
+    void *midend = CX_LL_PTR(nright, ll->loc_prev);
 
     if (prev == NULL) {
         ll->begin = nright;
     } else {
-        prev->next = nright;
+        CX_LL_PTR(prev, ll->loc_next) = nright;
     }
-    nright->prev = prev;
+    CX_LL_PTR(nright, ll->loc_prev) = prev;
     if (midstart == nright) {
         // special case: both nodes are adjacent
-        nright->next = nleft;
-        nleft->prev = nright;
+        CX_LL_PTR(nright, ll->loc_next) = nleft;
+        CX_LL_PTR(nleft, ll->loc_prev) = nright;
     } else {
         // likely case: a chain is between the two nodes
-        nright->next = midstart;
-        midstart->prev = nright;
-        midend->next = nleft;
-        nleft->prev = midend;
+        CX_LL_PTR(nright, ll->loc_next) = midstart;
+        CX_LL_PTR(midstart, ll->loc_prev) = nright;
+        CX_LL_PTR(midend, ll->loc_next) = nleft;
+        CX_LL_PTR(nleft, ll->loc_prev) = midend;
     }
-    nleft->next = next;
+    CX_LL_PTR(nleft, ll->loc_next) = next;
     if (next == NULL) {
         ll->end = nleft;
     } else {
-        next->prev = nleft;
+        CX_LL_PTR(next, ll->loc_prev) = nleft;
     }
 
     return 0;
@@ -928,8 +1063,8 @@
         size_t index
 ) {
     cx_linked_list *ll = (cx_linked_list *) list;
-    cx_linked_list_node *node = cx_ll_node_at(ll, index);
-    return node == NULL ? NULL : node->payload;
+    char *node = cx_ll_node_at(ll, index);
+    return node == NULL ? NULL : node + ll->loc_data;
 }
 
 static size_t cx_ll_find_remove(
@@ -940,10 +1075,10 @@
     if (list->collection.size == 0) return 0;
 
     size_t index;
-    cx_linked_list *ll = ((cx_linked_list *) list);
-    cx_linked_list_node *node = cx_linked_list_find(
+    cx_linked_list *ll = (cx_linked_list *) list;
+    char *node = cx_linked_list_find(
             ll->begin,
-            CX_LL_LOC_NEXT, CX_LL_LOC_DATA,
+            ll->loc_next, ll->loc_data,
             list->collection.cmpfunc, elem,
             &index
     );
@@ -951,9 +1086,9 @@
         return list->collection.size;
     }
     if (remove) {
-        cx_invoke_destructor(list, node->payload);
-        cx_linked_list_remove((void **) &ll->begin, (void **) &ll->end,
-                              CX_LL_LOC_PREV, CX_LL_LOC_NEXT, node);
+        cx_invoke_destructor(list, node + ll->loc_data);
+        cx_linked_list_remove(&ll->begin, &ll->end,
+                              ll->loc_prev, ll->loc_next, node);
         list->collection.size--;
         cxFree(list->collection.allocator, node);
     }
@@ -962,14 +1097,14 @@
 
 static void cx_ll_sort(struct cx_list_s *list) {
     cx_linked_list *ll = (cx_linked_list *) list;
-    cx_linked_list_sort((void **) &ll->begin, (void **) &ll->end,
-                        CX_LL_LOC_PREV, CX_LL_LOC_NEXT, CX_LL_LOC_DATA,
+    cx_linked_list_sort(&ll->begin, &ll->end,
+                        ll->loc_prev, ll->loc_next, ll->loc_data,
                         list->collection.cmpfunc);
 }
 
 static void cx_ll_reverse(struct cx_list_s *list) {
     cx_linked_list *ll = (cx_linked_list *) list;
-    cx_linked_list_reverse((void **) &ll->begin, (void **) &ll->end, CX_LL_LOC_PREV, CX_LL_LOC_NEXT);
+    cx_linked_list_reverse(&ll->begin, &ll->end, ll->loc_prev, ll->loc_next);
 }
 
 static int cx_ll_compare(
@@ -978,8 +1113,10 @@
 ) {
     cx_linked_list *left = (cx_linked_list *) list;
     cx_linked_list *right = (cx_linked_list *) other;
+    assert(left->loc_next == right->loc_next);
+    assert(left->loc_data == right->loc_data);
     return cx_linked_list_compare(left->begin, right->begin,
-                                  CX_LL_LOC_NEXT, CX_LL_LOC_DATA,
+                                  left->loc_next, left->loc_data,
                                   list->collection.cmpfunc);
 }
 
@@ -990,49 +1127,52 @@
 
 static void cx_ll_iter_next(void *it) {
     struct cx_iterator_s *iter = it;
+    cx_linked_list *ll = iter->src_handle;
     if (iter->base.remove) {
         iter->base.remove = false;
-        struct cx_list_s *list = iter->src_handle.m;
-        cx_linked_list *ll = iter->src_handle.m;
-        cx_linked_list_node *node = iter->elem_handle;
-        iter->elem_handle = node->next;
-        cx_invoke_destructor(list, node->payload);
-        cx_linked_list_remove((void **) &ll->begin, (void **) &ll->end,
-                              CX_LL_LOC_PREV, CX_LL_LOC_NEXT, node);
+        struct cx_list_s *list = iter->src_handle;
+        char *node = iter->elem_handle;
+        iter->elem_handle = CX_LL_PTR(node, ll->loc_next);
+        cx_invoke_destructor(list, node + ll->loc_data);
+        cx_linked_list_remove(&ll->begin, &ll->end,
+                              ll->loc_prev, ll->loc_next, node);
         list->collection.size--;
+        iter->elem_count--;
         cxFree(list->collection.allocator, node);
     } else {
         iter->index++;
-        cx_linked_list_node *node = iter->elem_handle;
-        iter->elem_handle = node->next;
+        void *node = iter->elem_handle;
+        iter->elem_handle = CX_LL_PTR(node, ll->loc_next);
     }
 }
 
 static void cx_ll_iter_prev(void *it) {
     struct cx_iterator_s *iter = it;
+    cx_linked_list *ll = iter->src_handle;
     if (iter->base.remove) {
         iter->base.remove = false;
-        struct cx_list_s *list = iter->src_handle.m;
-        cx_linked_list *ll = iter->src_handle.m;
-        cx_linked_list_node *node = iter->elem_handle;
-        iter->elem_handle = node->prev;
+        struct cx_list_s *list = iter->src_handle;
+        char *node = iter->elem_handle;
+        iter->elem_handle = CX_LL_PTR(node, ll->loc_prev);
         iter->index--;
-        cx_invoke_destructor(list, node->payload);
-        cx_linked_list_remove((void **) &ll->begin, (void **) &ll->end,
-                              CX_LL_LOC_PREV, CX_LL_LOC_NEXT, node);
+        cx_invoke_destructor(list, node + ll->loc_data);
+        cx_linked_list_remove(&ll->begin, &ll->end,
+                              ll->loc_prev, ll->loc_next, node);
         list->collection.size--;
+        iter->elem_count--;
         cxFree(list->collection.allocator, node);
     } else {
         iter->index--;
-        cx_linked_list_node *node = iter->elem_handle;
-        iter->elem_handle = node->prev;
+        char *node = iter->elem_handle;
+        iter->elem_handle = CX_LL_PTR(node, ll->loc_prev);
     }
 }
 
 static void *cx_ll_iter_current(const void *it) {
     const struct cx_iterator_s *iter = it;
-    cx_linked_list_node *node = iter->elem_handle;
-    return node->payload;
+    const cx_linked_list *ll = iter->src_handle;
+    char *node = iter->elem_handle;
+    return node + ll->loc_data;
 }
 
 static CxIterator cx_ll_iterator(
@@ -1042,14 +1182,14 @@
 ) {
     CxIterator iter;
     iter.index = index;
-    iter.src_handle.c = list;
+    iter.src_handle = (void*)list;
     iter.elem_handle = cx_ll_node_at((const cx_linked_list *) list, index);
     iter.elem_size = list->collection.elem_size;
     iter.elem_count = list->collection.size;
     iter.base.valid = cx_ll_iter_valid;
     iter.base.current = cx_ll_iter_current;
     iter.base.next = backwards ? cx_ll_iter_prev : cx_ll_iter_next;
-    iter.base.mutating = false;
+    iter.base.allow_remove = true;
     iter.base.remove = false;
     return iter;
 }
@@ -1059,11 +1199,12 @@
         const void *elem,
         int prepend
 ) {
-    struct cx_list_s *list = iter->src_handle.m;
-    cx_linked_list_node *node = iter->elem_handle;
+    struct cx_list_s *list = iter->src_handle;
+    cx_linked_list *ll = iter->src_handle;
+    void *node = iter->elem_handle;
     if (node != NULL) {
         assert(prepend >= 0 && prepend <= 1);
-        cx_linked_list_node *choice[2] = {node, node->prev};
+        void *choice[2] = {node, CX_LL_PTR(node, ll->loc_prev)};
         int result = cx_ll_insert_at(list, choice[prepend], elem);
         if (result == 0) {
             iter->elem_count++;
@@ -1085,10 +1226,10 @@
 static void cx_ll_destructor(CxList *list) {
     cx_linked_list *ll = (cx_linked_list *) list;
 
-    cx_linked_list_node *node = ll->begin;
+    char *node = ll->begin;
     while (node) {
-        cx_invoke_destructor(list, node->payload);
-        void *next = node->next;
+        cx_invoke_destructor(list, node + ll->loc_data);
+        void *next = CX_LL_PTR(node, ll->loc_next);
         cxFree(list->collection.allocator, node);
         node = next;
     }
@@ -1101,6 +1242,7 @@
         cx_ll_insert_element,
         cx_ll_insert_array,
         cx_ll_insert_sorted,
+        cx_ll_insert_unique,
         cx_ll_insert_iter,
         cx_ll_remove,
         cx_ll_clear,
@@ -1124,6 +1266,10 @@
 
     cx_linked_list *list = cxCalloc(allocator, 1, sizeof(cx_linked_list));
     if (list == NULL) return NULL;
+    list->extra_data_len = 0;
+    list->loc_prev = 0;
+    list->loc_next = sizeof(void*);
+    list->loc_data = sizeof(void*)*2;
     cx_list_init((CxList*)list, &cx_linked_list_class,
             allocator, comparator, elem_size);
 
--- a/ucx/list.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/list.c	Sat Nov 08 23:06:11 2025 +0100
@@ -29,6 +29,7 @@
 #include "cx/list.h"
 
 #include <string.h>
+#include <assert.h>
 
 // <editor-fold desc="Store Pointers Functionality">
 
@@ -38,10 +39,18 @@
         const void *l,
         const void *r
 ) {
+    // l and r are guaranteed to be non-NULL pointing to the list's memory
     void *const *lptr = l;
     void *const *rptr = r;
-    const void *left = lptr == NULL ? NULL : *lptr;
-    const void *right = rptr == NULL ? NULL : *rptr;
+    const void *left = *lptr;
+    const void *right = *rptr;
+    if (left == NULL) {
+        // NULL is smaller than any value except NULL
+        return right == NULL ? 0 : -1;
+    } else if (right == NULL) {
+        // any value is larger than NULL
+        return 1;
+    }
     return cx_pl_cmpfunc_impl(left, right);
 }
 
@@ -90,12 +99,23 @@
     return result;
 }
 
+static size_t cx_pl_insert_unique(
+        struct cx_list_s *list,
+        const void *array,
+        size_t n
+) {
+    cx_pl_hack_cmpfunc(list);
+    size_t result = list->climpl->insert_unique(list, array, n);
+    cx_pl_unhack_cmpfunc(list);
+    return result;
+}
+
 static int cx_pl_insert_iter(
         struct cx_iterator_s *iter,
         const void *elem,
         int prepend
 ) {
-    struct cx_list_s *list = iter->src_handle.m;
+    struct cx_list_s *list = iter->src_handle;
     return list->climpl->insert_iter(iter, &elem, prepend);
 }
 
@@ -181,6 +201,7 @@
         cx_pl_insert_element,
         cx_pl_insert_array,
         cx_pl_insert_sorted,
+        cx_pl_insert_unique,
         cx_pl_insert_iter,
         cx_pl_remove,
         cx_pl_clear,
@@ -225,7 +246,7 @@
         cx_attr_unused bool backwards
 ) {
     CxIterator iter = {0};
-    iter.src_handle.c = list;
+    iter.src_handle = (void*) list;
     iter.index = index;
     iter.base.valid = cx_emptyl_iter_valid;
     return iter;
@@ -238,6 +259,7 @@
         NULL,
         NULL,
         NULL,
+        NULL,
         cx_emptyl_noop,
         NULL,
         cx_emptyl_at,
@@ -278,21 +300,26 @@
         const void *data,
         size_t n
 ) {
-    size_t elem_size = list->collection.elem_size;
     const char *src = data;
     size_t i = 0;
     for (; i < n; i++) {
         if (NULL == invoke_list_func(
-            insert_element, list, index + i,
-            src + (i * elem_size))) return i;
+            insert_element, list, index + i, src)
+        ) {
+            return i; // LCOV_EXCL_LINE
+        }
+        if (src != NULL) {
+            src += list->collection.elem_size;
+        }
     }
     return i;
 }
 
-size_t cx_list_default_insert_sorted(
+static size_t cx_list_default_insert_sorted_impl(
         struct cx_list_s *list,
         const void *sorted_data,
-        size_t n
+        size_t n,
+        bool allow_duplicates
 ) {
     // corner case
     if (n == 0) return 0;
@@ -302,22 +329,54 @@
     const char *src = sorted_data;
 
     // track indices and number of inserted items
-    size_t di = 0, si = 0, inserted = 0;
+    size_t di = 0, si = 0, processed = 0;
 
     // search the list for insertion points
-    for (; di < list->collection.size; di++) {
+    while (di < list->collection.size) {
         const void *list_elm = invoke_list_func(at, list, di);
 
-        // compare current list element with first source element
-        // if less or equal, skip
-        if (cmp(list_elm, src) <= 0) {
-            continue;
+        // compare the current list element with the first source element
+        // if less, skip the list elements
+        // if equal, skip the list elements and optionally the source elements
+        {
+            int d = cmp(list_elm, src);
+            if (d <= 0) {
+                if (!allow_duplicates && d == 0) {
+                    src += elem_size;
+                    si++;
+                    processed++; // we also count duplicates for the return value
+                    while (si < n && cmp(list_elm, src) == 0) {
+                        src += elem_size;
+                        si++;
+                        processed++;
+                    }
+                    if (processed == n) {
+                        return processed;
+                    }
+                }
+                di++;
+                continue;
+            }
         }
 
-        // determine number of consecutive elements that can be inserted
-        size_t ins = 1;
+        // determine the number of consecutive elements that can be inserted
+        size_t ins = 1, skip = 0;
         const char *next = src;
         while (++si < n) {
+            if (!allow_duplicates) {
+                // skip duplicates within the source
+                if (cmp(next, next + elem_size) == 0) {
+                    next += elem_size;
+                    skip++;
+                    continue;
+                } else {
+                    if (skip > 0) {
+                        // if we had to skip something, we must wait for the next run
+                        next += elem_size;
+                        break;
+                    }
+                }
+            }
             next += elem_size;
             // once we become larger than the list elem, break
             if (cmp(list_elm, next) <= 0) {
@@ -329,33 +388,70 @@
 
         // insert the elements at location si
         if (ins == 1) {
-            if (NULL == invoke_list_func(
-                insert_element, list, di, src)) return inserted;
+            if (NULL == invoke_list_func(insert_element, list, di, src)) {
+                return processed; // LCOV_EXCL_LINE
+            }
         } else {
             size_t r = invoke_list_func(insert_array, list, di, src, ins);
-            if (r < ins) return inserted + r;
+            if (r < ins) {
+                return processed + r;  // LCOV_EXCL_LINE
+            }
         }
-        inserted += ins;
+        processed += ins + skip;
         di += ins;
 
         // everything inserted?
-        if (inserted == n) return inserted;
+        if (processed == n) {
+            return processed;
+        }
         src = next;
     }
 
     // insert remaining items
     if (si < n) {
-        inserted += invoke_list_func(insert_array, list, di, src, n - si);
+        if (allow_duplicates) {
+            processed += invoke_list_func(insert_array, list, di, src, n - si);
+        } else {
+            const void *last = di == 0 ? NULL : invoke_list_func(at, list, di - 1);
+            for (; si < n; si++) {
+                // skip duplicates within the source
+                if (last == NULL || cmp(last, src) != 0) {
+                    if (NULL == invoke_list_func(insert_element, list, di, src)) {
+                        return processed; // LCOV_EXCL_LINE
+                    }
+                    last = src;
+                    di++;
+                }
+                processed++;
+                src += elem_size;
+            }
+        }
     }
 
-    return inserted;
+    return processed;
+}
+
+size_t cx_list_default_insert_sorted(
+        struct cx_list_s *list,
+        const void *sorted_data,
+        size_t n
+) {
+    return cx_list_default_insert_sorted_impl(list, sorted_data, n, true);
+}
+
+size_t cx_list_default_insert_unique(
+        struct cx_list_s *list,
+        const void *sorted_data,
+        size_t n
+) {
+    return cx_list_default_insert_sorted_impl(list, sorted_data, n, false);
 }
 
 void cx_list_default_sort(struct cx_list_s *list) {
     size_t elem_size = list->collection.elem_size;
     size_t list_size = list->collection.size;
     void *tmp = cxMallocDefault(elem_size * list_size);
-    if (tmp == NULL) abort();
+    if (tmp == NULL) abort(); // LCOV_EXCL_LINE
 
     // copy elements from source array
     char *loc = tmp;
@@ -388,7 +484,7 @@
     size_t elem_size = list->collection.elem_size;
 
     void *tmp = cxMallocDefault(elem_size);
-    if (tmp == NULL) return 1;
+    if (tmp == NULL) return 1; // LCOV_EXCL_LINE
 
     void *ip = invoke_list_func(at, list, i);
     void *jp = invoke_list_func(at, list, j);
@@ -472,36 +568,174 @@
     }
 }
 
-CxIterator cxListMutIteratorAt(
-        CxList *list,
-        size_t index
-) {
-    if (list == NULL) list = cxEmptyList;
-    CxIterator it = list->cl->iterator(list, index, false);
-    it.base.mutating = true;
-    return it;
+size_t cxListSize(const CxList *list) {
+    return list->collection.size;
+}
+
+int cxListAdd(CxList *list, const void *elem) {
+    list->collection.sorted = false;
+    return list->cl->insert_element(list, list->collection.size, elem) == NULL;
+}
+
+size_t cxListAddArray(CxList *list, const void *array, size_t n) {
+    list->collection.sorted = false;
+    return list->cl->insert_array(list, list->collection.size, array, n);
+}
+
+int cxListInsert(CxList *list, size_t index, const void *elem) {
+    list->collection.sorted = false;
+    return list->cl->insert_element(list, index, elem) == NULL;
+}
+
+void *cxListEmplaceAt(CxList *list, size_t index) {
+    list->collection.sorted = false;
+    return list->cl->insert_element(list, index, NULL);
+}
+
+void *cxListEmplace(CxList *list) {
+    list->collection.sorted = false;
+    return list->cl->insert_element(list, list->collection.size, NULL);
+}
+
+static bool cx_list_emplace_iterator_valid(const void *it) {
+    const CxIterator *iter = it;
+    return iter->index < iter->elem_count;
+}
+
+CxIterator cxListEmplaceArrayAt(CxList *list, size_t index, size_t n) {
+    list->collection.sorted = false;
+    size_t c = list->cl->insert_array(list, index, NULL, n);
+    CxIterator iter = list->cl->iterator(list, index, false);
+    // tweak the fields of this iterator
+    iter.elem_count = c;
+    iter.index = 0;
+    // replace the valid function to abort iteration when c is reached
+    iter.base.valid = cx_list_emplace_iterator_valid;
+    // if we are storing pointers, we want to return the pure pointers.
+    // therefore, we must unwrap the "current" method
+    if (list->collection.store_pointer) {
+        iter.base.current = iter.base.current_impl;
+    }
+    return iter;
+}
+
+CxIterator cxListEmplaceArray(CxList *list, size_t n) {
+    return cxListEmplaceArrayAt(list, list->collection.size, n);
+}
+
+int cxListInsertSorted(CxList *list, const void *elem) {
+    assert(cxCollectionSorted(list));
+    list->collection.sorted = true;
+    const void *data = list->collection.store_pointer ? &elem : elem;
+    return list->cl->insert_sorted(list, data, 1) == 0;
+}
+
+int cxListInsertUnique(CxList *list, const void *elem) {
+    if (cxCollectionSorted(list)) {
+        list->collection.sorted = true;
+        const void *data = list->collection.store_pointer ? &elem : elem;
+        return list->cl->insert_unique(list, data, 1) == 0;
+    } else {
+        if (cxListContains(list, elem)) {
+            return 0;
+        } else {
+            return cxListAdd(list, elem);
+        }
+    }
+}
+
+size_t cxListInsertArray(CxList *list, size_t index, const void *array, size_t n) {
+    list->collection.sorted = false;
+    return list->cl->insert_array(list, index, array, n);
 }
 
-CxIterator cxListMutBackwardsIteratorAt(
-        CxList *list,
-        size_t index
-) {
-    if (list == NULL) list = cxEmptyList;
-    CxIterator it = list->cl->iterator(list, index, true);
-    it.base.mutating = true;
-    return it;
+size_t cxListInsertSortedArray(CxList *list, const void *array, size_t n) {
+    assert(cxCollectionSorted(list));
+    list->collection.sorted = true;
+    return list->cl->insert_sorted(list, array, n);
+}
+
+size_t cxListInsertUniqueArray(CxList *list, const void *array, size_t n) {
+    if (cxCollectionSorted(list)) {
+        list->collection.sorted = true;
+        return list->cl->insert_unique(list, array, n);
+    } else {
+        const char *source = array;
+        for (size_t i = 0 ; i < n; i++) {
+            // note: this also checks elements added in a previous iteration
+            const void *data = list->collection.store_pointer ?
+                    *((const void**)source) : source;
+            if (!cxListContains(list, data)) {
+                if (cxListAdd(list, data)) {
+                    return i; // LCOV_EXCL_LINE
+                }
+            }
+            source += list->collection.elem_size;
+        }
+        return n;
+    }
+}
+
+int cxListInsertAfter(CxIterator *iter, const void *elem) {
+    CxList* list = (CxList*)iter->src_handle;
+    list->collection.sorted = false;
+    return list->cl->insert_iter(iter, elem, 0);
+}
+
+int cxListInsertBefore(CxIterator *iter, const void *elem) {
+    CxList* list = (CxList*)iter->src_handle;
+    list->collection.sorted = false;
+    return list->cl->insert_iter(iter, elem, 1);
+}
+
+int cxListRemove(CxList *list, size_t index) {
+    return list->cl->remove(list, index, 1, NULL) == 0;
 }
 
-void cxListFree(CxList *list) {
-    if (list == NULL) return;
-    list->cl->deallocate(list);
+int cxListRemoveAndGet(CxList *list, size_t index, void *targetbuf) {
+    return list->cl->remove(list, index, 1, targetbuf) == 0;
+}
+
+int cxListRemoveAndGetFirst(CxList *list, void *targetbuf) {
+    return list->cl->remove(list, 0, 1, targetbuf) == 0;
+}
+
+int cxListRemoveAndGetLast(CxList *list, void *targetbuf) {
+    // note: index may wrap - member function will catch that
+    return list->cl->remove(list, list->collection.size - 1, 1, targetbuf) == 0;
+}
+
+size_t cxListRemoveArray(CxList *list, size_t index, size_t num) {
+    return list->cl->remove(list, index, num, NULL);
+}
+
+size_t cxListRemoveArrayAndGet(CxList *list, size_t index, size_t num, void *targetbuf) {
+    return list->cl->remove(list, index, num, targetbuf);
 }
 
-int cxListSet(
-        CxList *list,
-        size_t index,
-        const void *elem
-) {
+void cxListClear(CxList *list) {
+    list->cl->clear(list);
+    list->collection.sorted = true; // empty lists are always sorted
+}
+
+int cxListSwap(CxList *list, size_t i, size_t j) {
+    list->collection.sorted = false;
+    return list->cl->swap(list, i, j);
+}
+
+void *cxListAt(const CxList *list, size_t index) {
+    return list->cl->at(list, index);
+}
+
+void *cxListFirst(const CxList *list) {
+    return list->cl->at(list, 0);
+}
+
+void *cxListLast(const CxList *list) {
+    return list->cl->at(list, list->collection.size - 1);
+}
+
+int cxListSet(CxList *list, size_t index, const void *elem) {
     if (index >= list->collection.size) {
         return 1;
     }
@@ -517,3 +751,330 @@
 
     return 0;
 }
+
+CxIterator cxListIteratorAt(const CxList *list, size_t index) {
+    if (list == NULL) list = cxEmptyList;
+    return list->cl->iterator(list, index, false);
+}
+
+CxIterator cxListBackwardsIteratorAt(const CxList *list, size_t index) {
+    if (list == NULL) list = cxEmptyList;
+    return list->cl->iterator(list, index, true);
+}
+
+CxIterator cxListIterator(const CxList *list) {
+    if (list == NULL) list = cxEmptyList;
+    return list->cl->iterator(list, 0, false);
+}
+
+CxIterator cxListBackwardsIterator(const CxList *list) {
+    if (list == NULL) list = cxEmptyList;
+    return list->cl->iterator(list, list->collection.size - 1, true);
+}
+
+size_t cxListFind(const CxList *list, const void *elem) {
+    return list->cl->find_remove((CxList*)list, elem, false);
+}
+
+bool cxListContains(const CxList* list, const void* elem) {
+    return list->cl->find_remove((CxList*)list, elem, false) < list->collection.size;
+}
+
+bool cxListIndexValid(const CxList *list, size_t index) {
+    return index < list->collection.size;
+}
+
+size_t cxListFindRemove(CxList *list, const void *elem) {
+    return list->cl->find_remove(list, elem, true);
+}
+
+void cxListSort(CxList *list) {
+    if (list->collection.sorted) return;
+    list->cl->sort(list);
+    list->collection.sorted = true;
+}
+
+void cxListReverse(CxList *list) {
+    // still sorted, but not according to the cmp_func
+    list->collection.sorted = false;
+    list->cl->reverse(list);
+}
+
+void cxListFree(CxList *list) {
+    if (list == NULL) return;
+    list->cl->deallocate(list);
+}
+
+static void cx_list_pop_uninitialized_elements(CxList *list, size_t n) {
+    cx_destructor_func destr_bak = list->collection.simple_destructor;
+    cx_destructor_func2 destr2_bak = list->collection.advanced_destructor;
+    list->collection.simple_destructor = NULL;
+    list->collection.advanced_destructor = NULL;
+    if (n == 1) {
+        cxListRemove(list, list->collection.size - 1);
+    } else {
+        cxListRemoveArray(list,list->collection.size - n, n);
+    }
+    list->collection.simple_destructor = destr_bak;
+    list->collection.advanced_destructor = destr2_bak;
+}
+
+int cxListClone(CxList *dst, const CxList *src, cx_clone_func clone_func,
+        const CxAllocator *clone_allocator, void *data) {
+    if (clone_allocator == NULL) clone_allocator = cxDefaultAllocator;
+
+    // remember the original size
+    size_t orig_size = dst->collection.size;
+
+    // first, try to allocate the memory in the new list
+    CxIterator empl_iter = cxListEmplaceArray(dst, src->collection.size);
+
+    // get an iterator over the source elements
+    CxIterator src_iter = cxListIterator(src);
+
+    // now clone the elements
+    size_t cloned = empl_iter.elem_count;
+    for (size_t i = 0 ; i < empl_iter.elem_count; i++) {
+        void *src_elem = cxIteratorCurrent(src_iter);
+        void **dest_memory = cxIteratorCurrent(empl_iter);
+        void *target = cxCollectionStoresPointers(dst) ? NULL : dest_memory;
+        void *dest_ptr = clone_func(target, src_elem, clone_allocator, data);
+        if (dest_ptr == NULL) {
+            cloned = i;
+            break;
+        }
+        if (cxCollectionStoresPointers(dst)) {
+            *dest_memory = dest_ptr;
+        }
+        cxIteratorNext(src_iter);
+        cxIteratorNext(empl_iter);
+    }
+
+    // if we could not clone everything, free the allocated memory
+    // (disable the destructors!)
+    if (cloned < src->collection.size) {
+        cx_list_pop_uninitialized_elements(dst,
+            dst->collection.size - cloned - orig_size);
+        return 1;
+    }
+
+    return 0;
+}
+
+int cxListDifference(CxList *dst,
+        const CxList *minuend, const CxList *subtrahend,
+        cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data) {
+    if (clone_allocator == NULL) clone_allocator = cxDefaultAllocator;
+
+    // optimize for sorted collections
+    if (cxCollectionSorted(minuend) && cxCollectionSorted(subtrahend)) {
+        bool dst_was_empty = cxCollectionSize(dst) == 0;
+
+        CxIterator min_iter = cxListIterator(minuend);
+        CxIterator sub_iter = cxListIterator(subtrahend);
+        while (cxIteratorValid(min_iter)) {
+            void *min_elem = cxIteratorCurrent(min_iter);
+            void *sub_elem;
+            int d;
+            if (cxIteratorValid(sub_iter)) {
+                sub_elem = cxIteratorCurrent(sub_iter);
+                cx_compare_func cmp = subtrahend->collection.cmpfunc;
+                d = cmp(sub_elem, min_elem);
+            } else {
+                // no more elements in the subtrahend,
+                // i.e., the min_elem is larger than any elem of the subtrahend
+                d = 1;
+            }
+            if (d == 0) {
+                // is contained, so skip it
+                cxIteratorNext(min_iter);
+            } else if (d < 0) {
+                // subtrahend is smaller than minuend,
+                // check the next element
+                cxIteratorNext(sub_iter);
+            } else {
+                // subtrahend is larger than the dst element,
+                // clone the minuend and advance
+                void **dst_mem = cxListEmplace(dst);
+                void *target = cxCollectionStoresPointers(dst) ? NULL : dst_mem;
+                void* dst_ptr = clone_func(target, min_elem, clone_allocator, data);
+                if (dst_ptr == NULL) {
+                    cx_list_pop_uninitialized_elements(dst, 1);
+                    return 1;
+                }
+                if (cxCollectionStoresPointers(dst)) {
+                    *dst_mem = dst_ptr;
+                }
+                cxIteratorNext(min_iter);
+            }
+        }
+
+        // if dst was empty, it is now guaranteed to be sorted
+        dst->collection.sorted = dst_was_empty;
+    } else {
+        CxIterator min_iter = cxListIterator(minuend);
+        cx_foreach(void *, elem, min_iter) {
+            if (cxListContains(subtrahend, elem)) {
+                continue;
+            }
+            void **dst_mem = cxListEmplace(dst);
+            void *target = cxCollectionStoresPointers(dst) ? NULL : dst_mem;
+            void* dst_ptr = clone_func(target, elem, clone_allocator, data);
+            if (dst_ptr == NULL) {
+                cx_list_pop_uninitialized_elements(dst, 1);
+                return 1;
+            }
+            if (cxCollectionStoresPointers(dst)) {
+                *dst_mem = dst_ptr;
+            }
+        }
+    }
+
+    return 0;
+}
+
+int cxListIntersection(CxList *dst,
+        const CxList *src, const CxList *other,
+        cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data) {
+    if (clone_allocator == NULL) clone_allocator = cxDefaultAllocator;
+
+    // optimize for sorted collections
+    if (cxCollectionSorted(src) && cxCollectionSorted(other)) {
+        bool dst_was_empty = cxCollectionSize(dst) == 0;
+
+        CxIterator src_iter = cxListIterator(src);
+        CxIterator other_iter = cxListIterator(other);
+        while (cxIteratorValid(src_iter) && cxIteratorValid(other_iter)) {
+            void *src_elem = cxIteratorCurrent(src_iter);
+            void *other_elem = cxIteratorCurrent(other_iter);
+            int d = src->collection.cmpfunc(src_elem, other_elem);
+            if (d == 0) {
+                // is contained, clone it
+                void **dst_mem = cxListEmplace(dst);
+                void *target = cxCollectionStoresPointers(dst) ? NULL : dst_mem;
+                void* dst_ptr = clone_func(target, src_elem, clone_allocator, data);
+                if (dst_ptr == NULL) {
+                    cx_list_pop_uninitialized_elements(dst, 1);
+                    return 1;
+                }
+                if (cxCollectionStoresPointers(dst)) {
+                    *dst_mem = dst_ptr;
+                }
+                cxIteratorNext(src_iter);
+            } else if (d < 0) {
+                // the other element is larger, skip the source element
+                cxIteratorNext(src_iter);
+            } else {
+                // the source element is larger, try to find it in the other list
+                cxIteratorNext(other_iter);
+            }
+        }
+
+        // if dst was empty, it is now guaranteed to be sorted
+        dst->collection.sorted = dst_was_empty;
+    } else {
+        CxIterator src_iter = cxListIterator(src);
+        cx_foreach(void *, elem, src_iter) {
+            if (!cxListContains(other, elem)) {
+                continue;
+            }
+            void **dst_mem = cxListEmplace(dst);
+            void *target = cxCollectionStoresPointers(dst) ? NULL : dst_mem;
+            void* dst_ptr = clone_func(target, elem, clone_allocator, data);
+            if (dst_ptr == NULL) {
+                cx_list_pop_uninitialized_elements(dst, 1);
+                return 1;
+            }
+            if (cxCollectionStoresPointers(dst)) {
+                *dst_mem = dst_ptr;
+            }
+        }
+    }
+
+    return 0;
+}
+
+int cxListUnion(CxList *dst,
+        const CxList *src, const CxList *other,
+        cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data) {
+    if (clone_allocator == NULL) clone_allocator = cxDefaultAllocator;
+
+    // optimize for sorted collections
+    if (cxCollectionSorted(src) && cxCollectionSorted(other)) {
+        bool dst_was_empty = cxCollectionSize(dst) == 0;
+
+        CxIterator src_iter = cxListIterator(src);
+        CxIterator other_iter = cxListIterator(other);
+        while (cxIteratorValid(src_iter) || cxIteratorValid(other_iter)) {
+            void *src_elem, *other_elem;
+            int d;
+            if (!cxIteratorValid(src_iter)) {
+                other_elem = cxIteratorCurrent(other_iter);
+                d = 1;
+            } else if (!cxIteratorValid(other_iter)) {
+                src_elem = cxIteratorCurrent(src_iter);
+                d = -1;
+            } else {
+                src_elem = cxIteratorCurrent(src_iter);
+                other_elem = cxIteratorCurrent(other_iter);
+                d = src->collection.cmpfunc(src_elem, other_elem);
+            }
+            if (d <= 0) {
+                // source element is smaller or equal, clone it
+                void **dst_mem = cxListEmplace(dst);
+                void *target = cxCollectionStoresPointers(dst) ? NULL : dst_mem;
+                void* dst_ptr = clone_func(target, src_elem, clone_allocator, data);
+                if (dst_ptr == NULL) {
+                    cx_list_pop_uninitialized_elements(dst, 1);
+                    return 1;
+                }
+                if (cxCollectionStoresPointers(dst)) {
+                    *dst_mem = dst_ptr;
+                }
+                cxIteratorNext(src_iter);
+                // if the other element was equal, skip it
+                if (d == 0) {
+                    cxIteratorNext(other_iter);
+                }
+            } else {
+                // the other element is smaller, clone it
+                void **dst_mem = cxListEmplace(dst);
+                void *target = cxCollectionStoresPointers(dst) ? NULL : dst_mem;
+                void* dst_ptr = clone_func(target, other_elem, clone_allocator, data);
+                if (dst_ptr == NULL) {
+                    cx_list_pop_uninitialized_elements(dst, 1);
+                    return 1;
+                }
+                if (cxCollectionStoresPointers(dst)) {
+                    *dst_mem = dst_ptr;
+                }
+                cxIteratorNext(other_iter);
+            }
+        }
+
+        // if dst was empty, it is now guaranteed to be sorted
+        dst->collection.sorted = dst_was_empty;
+    } else {
+        if (cxListClone(dst, src, clone_func, clone_allocator, data)) {
+            return 1;
+        }
+        CxIterator other_iter = cxListIterator(other);
+        cx_foreach(void *, elem, other_iter) {
+            if (cxListContains(src, elem)) {
+                continue;
+            }
+            void **dst_mem = cxListEmplace(dst);
+            void *target = cxCollectionStoresPointers(dst) ? NULL : dst_mem;
+            void* dst_ptr = clone_func(target, elem, clone_allocator, data);
+            if (dst_ptr == NULL) {
+                cx_list_pop_uninitialized_elements(dst, 1);
+                return 1;
+            }
+            if (cxCollectionStoresPointers(dst)) {
+                *dst_mem = dst_ptr;
+            }
+        }
+    }
+
+    return 0;
+}
--- a/ucx/map.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/map.c	Sat Nov 08 23:06:11 2025 +0100
@@ -29,6 +29,8 @@
 #include "cx/map.h"
 #include <string.h>
 
+#include "cx/list.h"
+
 // <editor-fold desc="empty map implementation">
 
 static void cx_empty_map_noop(cx_attr_unused CxMap *map) {
@@ -51,7 +53,7 @@
         cx_attr_unused enum cx_map_iterator_type type
 ) {
     CxMapIterator iter = {0};
-    iter.map.c = map;
+    iter.map = (CxMap*) map;
     iter.base.valid = cx_empty_map_iter_valid;
     return iter;
 }
@@ -84,28 +86,210 @@
 
 // </editor-fold>
 
-CxMapIterator cxMapMutIteratorValues(CxMap *map) {
+void cxMapClear(CxMap *map) {
+    map->cl->clear(map);
+}
+
+size_t cxMapSize(const CxMap *map) {
+    return map->collection.size;
+}
+
+CxMapIterator cxMapIteratorValues(const CxMap *map) {
     if (map == NULL) map = cxEmptyMap;
-    CxMapIterator it = map->cl->iterator(map, CX_MAP_ITERATOR_VALUES);
-    it.base.mutating = true;
-    return it;
+    return map->cl->iterator(map, CX_MAP_ITERATOR_VALUES);
+}
+
+CxMapIterator cxMapIteratorKeys(const CxMap *map) {
+    if (map == NULL) map = cxEmptyMap;
+    return map->cl->iterator(map, CX_MAP_ITERATOR_KEYS);
 }
 
-CxMapIterator cxMapMutIteratorKeys(CxMap *map) {
+CxMapIterator cxMapIterator(const CxMap *map) {
     if (map == NULL) map = cxEmptyMap;
-    CxMapIterator it = map->cl->iterator(map, CX_MAP_ITERATOR_KEYS);
-    it.base.mutating = true;
-    return it;
+    return map->cl->iterator(map, CX_MAP_ITERATOR_PAIRS);
+}
+
+int cx_map_put(CxMap *map, CxHashKey key, void *value) {
+    return map->cl->put(map, key, value) == NULL;
 }
 
-CxMapIterator cxMapMutIterator(CxMap *map) {
-    if (map == NULL) map = cxEmptyMap;
-    CxMapIterator it = map->cl->iterator(map, CX_MAP_ITERATOR_PAIRS);
-    it.base.mutating = true;
-    return it;
+void *cx_map_emplace(CxMap *map, CxHashKey key) {
+    return map->cl->put(map, key, NULL);
+}
+
+void *cx_map_get(const CxMap *map, CxHashKey key) {
+    return map->cl->get(map, key);
+}
+
+int cx_map_remove(CxMap *map, CxHashKey key, void *targetbuf) {
+    return map->cl->remove(map, key, targetbuf);
 }
 
 void cxMapFree(CxMap *map) {
     if (map == NULL) return;
     map->cl->deallocate(map);
 }
+
+static void cx_map_remove_uninitialized_entry(CxMap *map, CxHashKey key) {
+    cx_destructor_func destr_bak = map->collection.simple_destructor;
+    cx_destructor_func2 destr2_bak = map->collection.advanced_destructor;
+    map->collection.simple_destructor = NULL;
+    map->collection.advanced_destructor = NULL;
+    cxMapRemove(map, key);
+    map->collection.simple_destructor = destr_bak;
+    map->collection.advanced_destructor = destr2_bak;
+}
+
+int cxMapClone(CxMap *dst, const CxMap *src, cx_clone_func clone_func,
+        const CxAllocator *clone_allocator, void *data) {
+    if (clone_allocator == NULL) clone_allocator = cxDefaultAllocator;
+    CxMapIterator src_iter = cxMapIterator(src);
+    for (size_t i = 0; i < cxMapSize(src); i++) {
+        const CxMapEntry *entry = cxIteratorCurrent(src_iter);
+        void **dst_mem = cxMapEmplace(dst, *(entry->key));
+        if (dst_mem == NULL) {
+            return 1; // LCOV_EXCL_LINE
+        }
+        void *target = cxCollectionStoresPointers(dst) ? NULL : dst_mem;
+        void *dst_ptr = clone_func(target, entry->value, clone_allocator, data);
+        if (dst_ptr == NULL) {
+            cx_map_remove_uninitialized_entry(dst, *(entry->key));
+            return 1;
+        }
+        if (cxCollectionStoresPointers(dst)) {
+            *dst_mem = dst_ptr;
+        }
+        cxIteratorNext(src_iter);
+    }
+    return 0;
+}
+
+int cxMapDifference(CxMap *dst, const CxMap *minuend, const CxMap *subtrahend,
+        cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data) {
+    if (clone_allocator == NULL) clone_allocator = cxDefaultAllocator;
+
+    CxMapIterator src_iter = cxMapIterator(minuend);
+    cx_foreach(const CxMapEntry *, entry, src_iter) {
+        if (cxMapContains(subtrahend, *entry->key)) {
+            continue;
+        }
+        void** dst_mem = cxMapEmplace(dst, *entry->key);
+        if (dst_mem == NULL) {
+            return 1; // LCOV_EXCL_LINE
+        }
+        void *target = cxCollectionStoresPointers(dst) ? NULL : dst_mem;
+        void* dst_ptr = clone_func(target, entry->value, clone_allocator, data);
+        if (dst_ptr == NULL) {
+            cx_map_remove_uninitialized_entry(dst, *(entry->key));
+            return 1;
+        }
+        if (cxCollectionStoresPointers(dst)) {
+            *dst_mem = dst_ptr;
+        }
+    }
+    return 0;
+}
+
+int cxMapListDifference(CxMap *dst, const CxMap *src, const CxList *keys,
+        cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data) {
+    if (clone_allocator == NULL) clone_allocator = cxDefaultAllocator;
+
+    CxMapIterator src_iter = cxMapIterator(src);
+    cx_foreach(const CxMapEntry *, entry, src_iter) {
+        if (cxListContains(keys, entry->key)) {
+            continue;
+        }
+        void** dst_mem = cxMapEmplace(dst, *entry->key);
+        if (dst_mem == NULL) {
+            return 1; // LCOV_EXCL_LINE
+        }
+        void *target = cxCollectionStoresPointers(dst) ? NULL : dst_mem;
+        void* dst_ptr = clone_func(target, entry->value, clone_allocator, data);
+        if (dst_ptr == NULL) {
+            cx_map_remove_uninitialized_entry(dst, *(entry->key));
+            return 1;
+        }
+        if (cxCollectionStoresPointers(dst)) {
+            *dst_mem = dst_ptr;
+        }
+    }
+    return 0;
+}
+
+int cxMapIntersection(CxMap *dst, const CxMap *src, const CxMap *other,
+        cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data) {
+    if (clone_allocator == NULL) clone_allocator = cxDefaultAllocator;
+
+    CxMapIterator src_iter = cxMapIterator(src);
+    cx_foreach(const CxMapEntry *, entry, src_iter) {
+        if (!cxMapContains(other, *entry->key)) {
+            continue;
+        }
+        void** dst_mem = cxMapEmplace(dst, *entry->key);
+        if (dst_mem == NULL) {
+            return 1; // LCOV_EXCL_LINE
+        }
+        void *target = cxCollectionStoresPointers(dst) ? NULL : dst_mem;
+        void* dst_ptr = clone_func(target, entry->value, clone_allocator, data);
+        if (dst_ptr == NULL) {
+            cx_map_remove_uninitialized_entry(dst, *(entry->key));
+            return 1;
+        }
+        if (cxCollectionStoresPointers(dst)) {
+            *dst_mem = dst_ptr;
+        }
+    }
+    return 0;
+}
+
+int cxMapListIntersection(CxMap *dst, const CxMap *src, const CxList *keys,
+        cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data) {
+    if (clone_allocator == NULL) clone_allocator = cxDefaultAllocator;
+
+    CxMapIterator src_iter = cxMapIterator(src);
+    cx_foreach(const CxMapEntry *, entry, src_iter) {
+        if (!cxListContains(keys, entry->key)) {
+            continue;
+        }
+        void** dst_mem = cxMapEmplace(dst, *entry->key);
+        if (dst_mem == NULL) {
+            return 1; // LCOV_EXCL_LINE
+        }
+        void *target = cxCollectionStoresPointers(dst) ? NULL : dst_mem;
+        void* dst_ptr = clone_func(target, entry->value, clone_allocator, data);
+        if (dst_ptr == NULL) {
+            cx_map_remove_uninitialized_entry(dst, *(entry->key));
+            return 1;
+        }
+        if (cxCollectionStoresPointers(dst)) {
+            *dst_mem = dst_ptr;
+        }
+    }
+    return 0;
+}
+
+int cxMapUnion(CxMap *dst, const CxMap *src,
+        cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data) {
+    if (clone_allocator == NULL) clone_allocator = cxDefaultAllocator;
+
+    CxMapIterator src_iter = cxMapIterator(src);
+    cx_foreach(const CxMapEntry *, entry, src_iter) {
+        if (cxMapContains(dst, *entry->key)) {
+            continue;
+        }
+        void** dst_mem = cxMapEmplace(dst, *entry->key);
+        if (dst_mem == NULL) {
+            return 1; // LCOV_EXCL_LINE
+        }
+        void *target = cxCollectionStoresPointers(dst) ? NULL : dst_mem;
+        void* dst_ptr = clone_func(target, entry->value, clone_allocator, data);
+        if (dst_ptr == NULL) {
+            cx_map_remove_uninitialized_entry(dst, *(entry->key));
+            return 1;
+        }
+        if (cxCollectionStoresPointers(dst)) {
+            *dst_mem = dst_ptr;
+        }
+    }
+    return 0;
+}
--- a/ucx/mempool.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/mempool.c	Sat Nov 08 23:06:11 2025 +0100
@@ -116,6 +116,9 @@
     if (!ptr) return;
     struct cx_mempool_s *pool = p;
 
+    cx_destructor_func destr = pool->destr;
+    cx_destructor_func2 destr2 = pool->destr2;
+
     struct cx_mempool_memory_s *mem =
         (void*) ((char *) ptr - sizeof(struct cx_mempool_memory_s));
 
@@ -124,11 +127,11 @@
             if (mem->destructor) {
                 mem->destructor(mem->c);
             }
-            if (pool->destr) {
-                pool->destr(mem->c);
+            if (destr != NULL) {
+                destr(mem->c);
             }
-            if (pool->destr2) {
-                pool->destr2(pool->destr2_data, mem->c);
+            if (destr2 != NULL) {
+                destr2(pool->destr2_data, mem->c);
             }
             cxFree(pool->base_allocator, mem);
             size_t last_index = pool->size - 1;
@@ -179,18 +182,18 @@
 }
 
 static void cx_mempool_free_all_simple(const struct cx_mempool_s *pool) {
-    const bool has_destr = pool->destr;
-    const bool has_destr2 = pool->destr2;
+    cx_destructor_func destr = pool->destr;
+    cx_destructor_func2 destr2 = pool->destr2;
     for (size_t i = 0; i < pool->size; i++) {
         struct cx_mempool_memory_s *mem = pool->data[i];
         if (mem->destructor) {
             mem->destructor(mem->c);
         }
-        if (has_destr) {
-            pool->destr(mem->c);
+        if (destr != NULL) {
+            destr(mem->c);
         }
-        if (has_destr2) {
-            pool->destr2(pool->destr2_data, mem->c);
+        if (destr2 != NULL) {
+            destr2(pool->destr2_data, mem->c);
         }
         cxFree(pool->base_allocator, mem);
     }
@@ -247,6 +250,9 @@
     if (!ptr) return;
     struct cx_mempool_s *pool = p;
 
+    cx_destructor_func destr = pool->destr;
+    cx_destructor_func2 destr2 = pool->destr2;
+
     struct cx_mempool_memory2_s *mem =
         (void*) ((char *) ptr - sizeof(struct cx_mempool_memory2_s));
 
@@ -255,11 +261,11 @@
             if (mem->destructor) {
                 mem->destructor(mem->data, mem->c);
             }
-            if (pool->destr) {
-                pool->destr(mem->c);
+            if (destr != NULL) {
+                destr(mem->c);
             }
-            if (pool->destr2) {
-                pool->destr2(pool->destr2_data, mem->c);
+            if (destr2 != NULL) {
+                destr2(pool->destr2_data, mem->c);
             }
             cxFree(pool->base_allocator, mem);
             size_t last_index = pool->size - 1;
@@ -310,18 +316,18 @@
 }
 
 static void cx_mempool_free_all_advanced(const struct cx_mempool_s *pool) {
-    const bool has_destr = pool->destr;
-    const bool has_destr2 = pool->destr2;
+    cx_destructor_func destr = pool->destr;
+    cx_destructor_func2 destr2 = pool->destr2;
     for (size_t i = 0; i < pool->size; i++) {
         struct cx_mempool_memory2_s *mem = pool->data[i];
         if (mem->destructor) {
             mem->destructor(mem->data, mem->c);
         }
-        if (has_destr) {
-            pool->destr(mem->c);
+        if (destr != NULL) {
+            destr(mem->c);
         }
-        if (has_destr2) {
-            pool->destr2(pool->destr2_data, mem->c);
+        if (destr2 != NULL) {
+            destr2(pool->destr2_data, mem->c);
         }
         cxFree(pool->base_allocator, mem);
     }
@@ -376,13 +382,16 @@
     if (!ptr) return;
     struct cx_mempool_s *pool = p;
 
+    cx_destructor_func destr = pool->destr;
+    cx_destructor_func2 destr2 = pool->destr2;
+
     for (size_t i = 0; i < pool->size; i++) {
         if (ptr == pool->data[i]) {
-            if (pool->destr) {
-                pool->destr(ptr);
+            if (destr != NULL) {
+                destr(ptr);
             }
-            if (pool->destr2) {
-                pool->destr2(pool->destr2_data, ptr);
+            if (destr2 != NULL) {
+                destr2(pool->destr2_data, ptr);
             }
             cxFree(pool->base_allocator, ptr);
             size_t last_index = pool->size - 1;
@@ -427,15 +436,15 @@
 }
 
 static void cx_mempool_free_all_pure(const struct cx_mempool_s *pool) {
-    const bool has_destr = pool->destr;
-    const bool has_destr2 = pool->destr2;
+    cx_destructor_func destr = pool->destr;
+    cx_destructor_func2 destr2 = pool->destr2;
     for (size_t i = 0; i < pool->size; i++) {
         void *mem = pool->data[i];
-        if (has_destr) {
-            pool->destr(mem);
+        if (destr != NULL) {
+            destr(mem);
         }
-        if (has_destr2) {
-            pool->destr2(pool->destr2_data, mem);
+        if (destr2 != NULL) {
+            destr2(pool->destr2_data, mem);
         }
         cxFree(pool->base_allocator, mem);
     }
--- a/ucx/properties.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/properties.c	Sat Nov 08 23:06:11 2025 +0100
@@ -51,6 +51,12 @@
     cxBufferDestroy(&prop->buffer);
 }
 
+void cxPropertiesReset(CxProperties *prop) {
+    CxPropertiesConfig config = prop->config;
+    cxPropertiesDestroy(prop);
+    cxPropertiesInit(prop, config);
+}
+
 int cxPropertiesFilln(
         CxProperties *prop,
         const char *buf,
@@ -244,7 +250,7 @@
     CxMap *map = sink->sink;
     CxAllocator *alloc = sink->data;
     cxmutstr v = cx_strdup_a(alloc, value);
-    int r = cx_map_put_cxstr(map, key, v.ptr);
+    int r = cxMapPut(map, key, v.ptr);
     if (r != 0) cx_strfree_a(alloc, &v);
     return r;
 }
--- a/ucx/string.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/string.c	Sat Nov 08 23:06:11 2025 +0100
@@ -25,6 +25,10 @@
  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  * POSSIBILITY OF SUCH DAMAGE.
  */
+#ifdef MEMRCHR_NEED_GNU
+#define _GNU_SOURCE
+#endif
+
 #include "cx/string.h"
 
 #include <string.h>
@@ -33,6 +37,7 @@
 #include <errno.h>
 #include <limits.h>
 #include <float.h>
+#include <ctype.h>
 
 #ifdef _WIN32
 #define cx_strcasecmp_impl _strnicmp
@@ -231,19 +236,24 @@
 }
 
 cxstring cx_strrchr(
-        cxstring string,
-        int chr
+    cxstring string,
+    int chr
 ) {
+#ifdef WITH_MEMRCHR
+    char *ret = memrchr(string.ptr, 0xFF & chr, string.length);
+    if (ret == NULL) return (cxstring) {NULL, 0};
+    return (cxstring) {ret, string.length - (ret - string.ptr)};
+#else
     chr = 0xFF & chr;
     size_t i = string.length;
     while (i > 0) {
         i--;
-        // TODO: improve by comparing multiple bytes at once
         if (string.ptr[i] == chr) {
             return cx_strsubs(string, i);
         }
     }
     return (cxstring) {NULL, 0};
+#endif
 }
 
 cxmutstr cx_strrchr_m(
@@ -451,7 +461,7 @@
                          delim, limit, (cxstring **) output);
 }
 
-int cx_strcmp(
+int cx_strcmp_(
         cxstring s1,
         cxstring s2
 ) {
@@ -468,7 +478,7 @@
     }
 }
 
-int cx_strcasecmp(
+int cx_strcasecmp_(
         cxstring s1,
         cxstring s2
 ) {
@@ -520,19 +530,13 @@
     return result;
 }
 
-static bool str_isspace(char c) {
-    // TODO: remove once UCX has public API for this
-    return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\v' || c == '\f';
-}
-
 cxstring cx_strtrim(cxstring string) {
     cxstring result = string;
-    // TODO: optimize by comparing multiple bytes at once
-    while (result.length > 0 && str_isspace(*result.ptr)) {
+    while (result.length > 0 && isspace((unsigned char)(result.ptr[0]))) {
         result.ptr++;
         result.length--;
     }
-    while (result.length > 0 && str_isspace(result.ptr[result.length - 1])) {
+    while (result.length > 0 && isspace((unsigned char)result.ptr[result.length - 1])) {
         result.length--;
     }
     return result;
@@ -543,7 +547,7 @@
     return (cxmutstr) {(char *) result.ptr, result.length};
 }
 
-bool cx_strprefix(
+bool cx_strprefix_(
         cxstring string,
         cxstring prefix
 ) {
@@ -551,7 +555,7 @@
     return memcmp(string.ptr, prefix.ptr, prefix.length) == 0;
 }
 
-bool cx_strsuffix(
+bool cx_strsuffix_(
         cxstring string,
         cxstring suffix
 ) {
@@ -560,7 +564,7 @@
                   suffix.ptr, suffix.length) == 0;
 }
 
-bool cx_strcaseprefix(
+bool cx_strcaseprefix_(
         cxstring string,
         cxstring prefix
 ) {
@@ -572,7 +576,7 @@
 #endif
 }
 
-bool cx_strcasesuffix(
+bool cx_strcasesuffix_(
         cxstring string,
         cxstring suffix
 ) {
@@ -957,11 +961,6 @@
     return 0;
 }
 
-static bool str_isdigit(char c) {
-    // TODO: remove once UCX has public API for this
-    return c >= '0' && c <= '9';
-}
-
 int cx_strtod_lc_(cxstring str, double *output, char decsep, const char *groupsep) {
     // TODO: overflow check
     // TODO: increase precision
@@ -994,7 +993,7 @@
     // parse all digits until we find the decsep
     size_t pos = 0;
     do {
-        if (str_isdigit(str.ptr[pos])) {
+        if (isdigit((unsigned char)str.ptr[pos])) {
             result = result * 10 + (str.ptr[pos] - '0');
         } else if (strchr(groupsep, str.ptr[pos]) == NULL) {
             break;
@@ -1023,7 +1022,7 @@
         // parse everything until exponent or end
         double factor = 1.;
         do {
-            if (str_isdigit(str.ptr[pos])) {
+            if (isdigit((unsigned char)str.ptr[pos])) {
                 factor *= 0.1;
                 result = result + factor * (str.ptr[pos] - '0');
             } else if (strchr(groupsep, str.ptr[pos]) == NULL) {
@@ -1064,7 +1063,7 @@
     // parse the exponent
     unsigned int exp = 0;
     do {
-        if (str_isdigit(str.ptr[pos])) {
+        if (isdigit((unsigned char)str.ptr[pos])) {
             exp = 10 * exp + (str.ptr[pos] - '0');
         } else if (strchr(groupsep, str.ptr[pos]) == NULL) {
             errno = EINVAL;
--- a/ucx/tree.c	Tue Oct 14 21:02:26 2025 +0200
+++ b/ucx/tree.c	Sat Nov 08 23:06:11 2025 +0100
@@ -375,7 +375,7 @@
     iter.skip = false;
 
     // assign base iterator functions
-    iter.base.mutating = false;
+    iter.base.allow_remove = false;
     iter.base.remove = false;
     iter.base.current_impl = NULL;
     iter.base.valid = cx_tree_iter_valid;
@@ -496,7 +496,7 @@
     iter.queue_last = NULL;
 
     // assign base iterator functions
-    iter.base.mutating = false;
+    iter.base.allow_remove = false;
     iter.base.remove = false;
     iter.base.current_impl = NULL;
     iter.base.valid = cx_tree_visitor_valid;
@@ -717,7 +717,7 @@
     }
 
     // otherwise, create iterator and hand over to other function
-    CxIterator iter = cxIterator(src, elem_size, num);
+    CxIterator iter = cxIterator(src, elem_size, num, false);
     return cx_tree_add_iter(cxIteratorRef(iter), num, sfunc,
                             cfunc, cdata, failed, root,
                             loc_parent, loc_children, loc_last_child,
@@ -804,16 +804,12 @@
         cx_tree_default_find
 };
 
-CxTree *cxTreeCreate(
-        const CxAllocator *allocator,
+CxTree *cxTreeCreate(const CxAllocator *allocator,
         cx_tree_node_create_func create_func,
         cx_tree_search_func search_func,
         cx_tree_search_data_func search_data_func,
-        ptrdiff_t loc_parent,
-        ptrdiff_t loc_children,
-        ptrdiff_t loc_last_child,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next
+        ptrdiff_t loc_parent, ptrdiff_t loc_children, ptrdiff_t loc_last_child,
+        ptrdiff_t loc_prev, ptrdiff_t loc_next
 ) {
     if (allocator == NULL) {
         allocator = cxDefaultAllocator;
@@ -852,15 +848,9 @@
     cxFree(tree->allocator, tree);
 }
 
-CxTree *cxTreeCreateWrapped(
-        const CxAllocator *allocator,
-        void *root,
-        ptrdiff_t loc_parent,
-        ptrdiff_t loc_children,
-        ptrdiff_t loc_last_child,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next
-) {
+CxTree *cxTreeCreateWrapped(const CxAllocator *allocator, void *root,
+        ptrdiff_t loc_parent, ptrdiff_t loc_children, ptrdiff_t loc_last_child,
+        ptrdiff_t loc_prev, ptrdiff_t loc_next) {
     if (allocator == NULL) {
         allocator = cxDefaultAllocator;
     }
@@ -888,11 +878,7 @@
     return tree;
 }
 
-void cxTreeSetParent(
-        CxTree *tree,
-        void *parent,
-        void *child
-) {
+void cxTreeSetParent(CxTree *tree, void *parent, void *child) {
     size_t loc_parent = tree->loc_parent;
     if (tree_parent(child) == NULL) {
         tree->size++;
@@ -900,19 +886,12 @@
     cx_tree_link(parent, child, cx_tree_node_layout(tree));
 }
 
-void cxTreeAddChildNode(
-        CxTree *tree,
-        void *parent,
-        void *child
-) {
+void cxTreeAddChildNode(CxTree *tree, void *parent, void *child) {
     cx_tree_link(parent, child, cx_tree_node_layout(tree));
     tree->size++;
 }
 
-int cxTreeAddChild(
-        CxTree *tree,
-        void *parent,
-        const void *data) {
+int cxTreeAddChild(CxTree *tree, void *parent, const void *data) {
     void *node = tree->node_create(data, tree);
     if (node == NULL) return 1;
     cx_tree_zero_pointers(node, cx_tree_node_layout(tree));
@@ -921,6 +900,29 @@
     return 0;
 }
 
+int cxTreeInsert(CxTree *tree, const void *data) {
+    return tree->cl->insert_element(tree, data);
+}
+
+size_t cxTreeInsertIter(CxTree *tree, CxIteratorBase *iter, size_t n) {
+    return tree->cl->insert_many(tree, iter, n);
+}
+
+size_t cxTreeInsertArray(CxTree *tree, const void *data, size_t elem_size, size_t n) {
+    if (n == 0) return 0;
+    if (n == 1) return 0 == cxTreeInsert(tree, data) ? 1 : 0;
+    CxIterator iter = cxIterator(data, elem_size, n, false);
+    return cxTreeInsertIter(tree, cxIteratorRef(iter), n);
+}
+
+void *cxTreeFind( CxTree *tree, const void *data) {
+    return tree->cl->find(tree, tree->root, data, 0);
+}
+
+void *cxTreeFindInSubtree(CxTree *tree, const void *data, void *subtree_root, size_t max_depth) {
+    return tree->cl->find(tree, subtree_root, data, max_depth);
+}
+
 size_t cxTreeSubtreeSize(CxTree *tree, void *subtree_root) {
     CxTreeVisitor visitor = cx_tree_visitor(
             subtree_root,
@@ -945,6 +947,10 @@
     return visitor.depth;
 }
 
+size_t cxTreeSize(CxTree *tree) {
+    return tree->size;
+}
+
 size_t cxTreeDepth(CxTree *tree) {
     CxTreeVisitor visitor = cx_tree_visitor(
             tree->root, tree->loc_children, tree->loc_next
@@ -1052,3 +1058,38 @@
         tree->root = NULL;
     }
 }
+
+void cxTreeIteratorDispose(CxTreeIterator *iter) {
+    cxFreeDefault(iter->stack);
+    iter->stack = NULL;
+}
+
+void cxTreeVisitorDispose(CxTreeVisitor *visitor) {
+    struct cx_tree_visitor_queue_s *q = visitor->queue_next;
+    while (q != NULL) {
+        struct cx_tree_visitor_queue_s *next = q->next;
+        cxFreeDefault(q);
+        q = next;
+    }
+}
+
+CxTreeIterator cxTreeIterateSubtree(CxTree *tree, void *node, bool visit_on_exit) {
+    return cx_tree_iterator(
+            node, visit_on_exit,
+            tree->loc_children, tree->loc_next
+    );
+}
+
+CxTreeVisitor cxTreeVisitSubtree(CxTree *tree, void *node) {
+    return cx_tree_visitor(
+            node, tree->loc_children, tree->loc_next
+    );
+}
+
+CxTreeIterator cxTreeIterate(CxTree *tree, bool visit_on_exit) {
+    return cxTreeIterateSubtree(tree, tree->root, visit_on_exit);
+}
+
+CxTreeVisitor cxTreeVisit(CxTree *tree) {
+    return cxTreeVisitSubtree(tree, tree->root);
+}

mercurial