update ucx default tip

Fri, 12 Dec 2025 10:42:53 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Fri, 12 Dec 2025 10:42:53 +0100
changeset 30
d33eaaec15da
parent 29
b8c826c720f3

update ucx

dbutils/json.c file | annotate | diff | comparison | revisions
test/json.c file | annotate | diff | comparison | revisions
ucx/allocator.c file | annotate | diff | comparison | revisions
ucx/buffer.c file | annotate | diff | comparison | revisions
ucx/cx/allocator.h file | annotate | diff | comparison | revisions
ucx/cx/buffer.h file | annotate | diff | comparison | revisions
ucx/cx/json.h file | annotate | diff | comparison | revisions
ucx/cx/properties.h file | annotate | diff | comparison | revisions
ucx/cx/tree.h file | annotate | diff | comparison | revisions
ucx/json.c file | annotate | diff | comparison | revisions
ucx/kv_list.c file | annotate | diff | comparison | revisions
ucx/properties.c file | annotate | diff | comparison | revisions
ucx/tree.c file | annotate | diff | comparison | revisions
--- a/dbutils/json.c	Thu Dec 11 22:58:02 2025 +0100
+++ b/dbutils/json.c	Fri Dec 12 10:42:53 2025 +0100
@@ -74,7 +74,7 @@
                     }
                     child->allocator = a;
                     child->type = CX_JSON_STRING;
-                    child->value.string = s;
+                    child->string = s;
                 }
             } else if(field->toBinary) {
                 // TODO
--- a/test/json.c	Thu Dec 11 22:58:02 2025 +0100
+++ b/test/json.c	Fri Dec 12 10:42:53 2025 +0100
@@ -164,17 +164,17 @@
         CX_TEST_ASSERT(u64 && u64->type == CX_JSON_INTEGER);
         CX_TEST_ASSERT(d && d->type == CX_JSON_NUMBER);
         
-        CX_TEST_ASSERT(!cx_strcmp(str->value.string, test.str));
-        CX_TEST_ASSERT(!cx_strcmp(str2->value.string, test.str2));
-        CX_TEST_ASSERT(i8->value.integer == test.i8);
-        CX_TEST_ASSERT(u8->value.integer == test.u8);
-        CX_TEST_ASSERT(i16->value.integer == test.i16);
-        CX_TEST_ASSERT(u16->value.integer == test.u16);
-        CX_TEST_ASSERT(i32->value.integer == test.i32);
-        CX_TEST_ASSERT(u32->value.integer == test.u32);
-        CX_TEST_ASSERT(i64->value.integer == test.i64);
-        CX_TEST_ASSERT(u64->value.integer == test.u64);
-        CX_TEST_ASSERT(d->value.number < test.d + 0.1 && d->value.number > test.d - 0.1);
+        CX_TEST_ASSERT(!cx_strcmp(str->string, test.str));
+        CX_TEST_ASSERT(!cx_strcmp(str2->string, test.str2));
+        CX_TEST_ASSERT(i8->integer == test.i8);
+        CX_TEST_ASSERT(u8->integer == test.u8);
+        CX_TEST_ASSERT(i16->integer == test.i16);
+        CX_TEST_ASSERT(u16->integer == test.u16);
+        CX_TEST_ASSERT(i32->integer == test.i32);
+        CX_TEST_ASSERT(u32->integer == test.u32);
+        CX_TEST_ASSERT(i64->integer == test.i64);
+        CX_TEST_ASSERT(u64->integer == test.u64);
+        CX_TEST_ASSERT(d->number < test.d + 0.1 && d->number > test.d - 0.1);
         
         cxJsonValueFree(value);
     }
@@ -207,7 +207,7 @@
         CxJsonValue *v = cxJsonObjGet(value, "id");
         CX_TEST_ASSERT(v);
         CX_TEST_ASSERT(cxJsonIsInteger(v));
-        CX_TEST_ASSERT(v->value.integer == t4.id);
+        CX_TEST_ASSERT(v->integer == t4.id);
         
         v = cxJsonObjGet(value, "test2");
         CX_TEST_ASSERT(v);
@@ -216,12 +216,12 @@
         CxJsonValue *s = cxJsonObjGet(v, "i");
         CX_TEST_ASSERT(s);
         CX_TEST_ASSERT(cxJsonIsInteger(s));
-        CX_TEST_ASSERT(s->value.integer == t2_2.i);
+        CX_TEST_ASSERT(s->integer == t2_2.i);
         
         s = cxJsonObjGet(v, "name");
         CX_TEST_ASSERT(s);
         CX_TEST_ASSERT(cxJsonIsString(s));
-        CX_TEST_ASSERT(!cx_strcmp(s->value.string, t2_2.name));
+        CX_TEST_ASSERT(!cx_strcmp(s->string, t2_2.name));
         
         v = cxJsonObjGet(value, "test3");
         CX_TEST_ASSERT(v);
@@ -230,7 +230,7 @@
         s = cxJsonObjGet(v, "test3");
         CX_TEST_ASSERT(s);
         CX_TEST_ASSERT(cxJsonIsString(s));
-        CX_TEST_ASSERT(!cx_strcmp(s->value.string, t3.test3));
+        CX_TEST_ASSERT(!cx_strcmp(s->string, t3.test3));
         
         s = cxJsonObjGet(v, "test2");
         CX_TEST_ASSERT(s);
@@ -239,12 +239,12 @@
         CxJsonValue *x = cxJsonObjGet(s, "i");
         CX_TEST_ASSERT(x);
         CX_TEST_ASSERT(cxJsonIsInteger(x));
-        CX_TEST_ASSERT(x->value.integer == t2_1.i);
+        CX_TEST_ASSERT(x->integer == t2_1.i);
         
         x = cxJsonObjGet(s, "name");
         CX_TEST_ASSERT(x);
         CX_TEST_ASSERT(cxJsonIsString(x));
-        CX_TEST_ASSERT(!cx_strcmp(x->value.string, t2_1.name));
+        CX_TEST_ASSERT(!cx_strcmp(x->string, t2_1.name));
         
         cxJsonValueFree(value);
     }
@@ -279,7 +279,7 @@
         CxJsonValue *v = cxJsonObjGet(value, "test");
         CX_TEST_ASSERT(v);
         CX_TEST_ASSERT(cxJsonIsString(v));
-        CX_TEST_ASSERT(!cx_strcmp(v->value.string, test5.test));
+        CX_TEST_ASSERT(!cx_strcmp(v->string, test5.test));
         
         v = cxJsonObjGet(value, "test2List");
         CX_TEST_ASSERT(v);
@@ -289,28 +289,28 @@
         CX_TEST_ASSERT(cxJsonIsObject(x));
         CxJsonValue *z = cxJsonObjGet(x, "i");
         CX_TEST_ASSERT(cxJsonIsInteger(z));
-        CX_TEST_ASSERT(z->value.integer == c1.i);
+        CX_TEST_ASSERT(z->integer == c1.i);
         z = cxJsonObjGet(x, "name");
         CX_TEST_ASSERT(cxJsonIsString(z));
-        CX_TEST_ASSERT(!cx_strcmp(z->value.string, c1.name));
+        CX_TEST_ASSERT(!cx_strcmp(z->string, c1.name));
         
         x = cxJsonArrGet(v, 1);
         CX_TEST_ASSERT(cxJsonIsObject(x));
         z = cxJsonObjGet(x, "i");
         CX_TEST_ASSERT(cxJsonIsInteger(z));
-        CX_TEST_ASSERT(z->value.integer == c2.i);
+        CX_TEST_ASSERT(z->integer == c2.i);
         z = cxJsonObjGet(x, "name");
         CX_TEST_ASSERT(cxJsonIsString(z));
-        CX_TEST_ASSERT(!cx_strcmp(z->value.string, c2.name));
+        CX_TEST_ASSERT(!cx_strcmp(z->string, c2.name));
         
         x = cxJsonArrGet(v, 2);
         CX_TEST_ASSERT(cxJsonIsObject(x));
         z = cxJsonObjGet(x, "i");
         CX_TEST_ASSERT(cxJsonIsInteger(z));
-        CX_TEST_ASSERT(z->value.integer == c3.i);
+        CX_TEST_ASSERT(z->integer == c3.i);
         z = cxJsonObjGet(x, "name");
         CX_TEST_ASSERT(cxJsonIsString(z));
-        CX_TEST_ASSERT(!cx_strcmp(z->value.string, c3.name));
+        CX_TEST_ASSERT(!cx_strcmp(z->string, c3.name));
         
         cxJsonValueFree(value);
     }
--- a/ucx/allocator.c	Thu Dec 11 22:58:02 2025 +0100
+++ b/ucx/allocator.c	Fri Dec 12 10:42:53 2025 +0100
@@ -31,6 +31,35 @@
 #include <errno.h>
 #include <string.h>
 
+#ifdef _WIN32
+#include <Windows.h>
+#include <sysinfoapi.h>
+unsigned long cx_system_page_size(void) {
+    static unsigned long ps = 0;
+    if (ps == 0) {
+        SYSTEM_INFO sysinfo;
+        GetSystemInfo(&sysinfo);
+        ps = (unsigned long) sysinfo.dwPageSize;
+    }
+    return ps;
+}
+#else
+#include <unistd.h>
+unsigned long cx_system_page_size(void) {
+    static unsigned long ps = 0;
+    if (ps == 0) {
+        long sc = sysconf(_SC_PAGESIZE);
+        if (sc < 0) {
+            // fallback for systems which do not report a value here
+            ps = 4096; // LCOV_EXCL_LINE
+        } else {
+            ps = (unsigned long) sc;
+        }
+    }
+    return ps;
+}
+#endif
+
 static void *cx_malloc_stdlib(
         cx_attr_unused void *d,
         size_t n
@@ -79,6 +108,11 @@
         void **mem,
         size_t n
 ) {
+    if (n == 0) {
+        free(*mem);
+        *mem = NULL;
+        return 0;
+    }
     void *nmem = realloc(*mem, n);
     if (nmem == NULL) {
         return 1; // LCOV_EXCL_LINE
@@ -93,6 +127,11 @@
         size_t nmemb,
         size_t size
 ) {
+    if (nmemb == 0 || size == 0) {
+        free(*mem);
+        *mem = NULL;
+        return 0;
+    }
     size_t n;
     if (cx_szmul(nmemb, size, &n)) {
         errno = EOVERFLOW;
@@ -156,6 +195,11 @@
         void **mem,
         size_t n
 ) {
+    if (n == 0) {
+        cxFree(allocator, *mem);
+        *mem = NULL;
+        return 0;
+    }
     void *nmem = allocator->cl->realloc(allocator->data, *mem, n);
     if (nmem == NULL) {
         return 1; // LCOV_EXCL_LINE
@@ -171,6 +215,11 @@
         size_t nmemb,
         size_t size
 ) {
+    if (nmemb == 0 || size == 0) {
+        cxFree(allocator, *mem);
+        *mem = NULL;
+        return 0;
+    }
     void *nmem = cxReallocArray(allocator, *mem, nmemb, size);
     if (nmem == NULL) {
         return 1; // LCOV_EXCL_LINE
@@ -194,3 +243,7 @@
 ) {
     allocator->cl->free(allocator->data, mem);
 }
+
+void cxFreeDefault(void *mem) {
+    cxDefaultAllocator->cl->free(cxDefaultAllocator->data, mem);
+}
--- a/ucx/buffer.c	Thu Dec 11 22:58:02 2025 +0100
+++ b/ucx/buffer.c	Fri Dec 12 10:42:53 2025 +0100
@@ -32,35 +32,6 @@
 #include <string.h>
 #include <errno.h>
 
-#ifdef _WIN32
-#include <Windows.h>
-#include <sysinfoapi.h>
-static unsigned long system_page_size(void) {
-    static unsigned long ps = 0;
-    if (ps == 0) {
-        SYSTEM_INFO sysinfo;
-        GetSystemInfo(&sysinfo);
-        ps = sysinfo.dwPageSize;
-    }
-    return ps;
-}
-#else
-#include <unistd.h>
-static unsigned long system_page_size(void) {
-    static unsigned long ps = 0;
-    if (ps == 0) {
-        long sc = sysconf(_SC_PAGESIZE);
-        if (sc < 0) {
-            // fallback for systems which do not report a value here
-            ps = 4096; // LCOV_EXCL_LINE
-        } else {
-            ps = (unsigned long) sc;
-        }
-    }
-    return ps;
-}
-#endif
-
 static int buffer_copy_on_write(CxBuffer* buffer) {
     if (0 == (buffer->flags & CX_BUFFER_COPY_ON_WRITE)) return 0;
     void *newspace = cxMalloc(buffer->allocator, buffer->capacity);
@@ -95,21 +66,10 @@
         buffer->bytes = space;
     }
     buffer->capacity = capacity;
+    buffer->max_capacity = SIZE_MAX;
     buffer->size = 0;
     buffer->pos = 0;
 
-    buffer->flush = NULL;
-
-    return 0;
-}
-
-int cxBufferEnableFlushing(
-    CxBuffer *buffer,
-    CxBufferFlushConfig config
-) {
-    buffer->flush = cxMallocDefault(sizeof(CxBufferFlushConfig));
-    if (buffer->flush == NULL) return -1; // LCOV_EXCL_LINE
-    memcpy(buffer->flush, &config, sizeof(CxBufferFlushConfig));
     return 0;
 }
 
@@ -117,7 +77,6 @@
     if (buffer->flags & CX_BUFFER_FREE_CONTENTS) {
         cxFree(buffer->allocator, buffer->bytes);
     }
-    cxFreeDefault(buffer->flush);
     memset(buffer, 0, sizeof(CxBuffer));
 }
 
@@ -239,9 +198,12 @@
 }
 
 int cxBufferReserve(CxBuffer *buffer, size_t newcap) {
-    if (newcap <= buffer->capacity) {
+    if (newcap == buffer->capacity) {
         return 0;
     }
+    if (newcap > buffer->max_capacity) {
+        return -1;
+    }
     const int force_copy_flags = CX_BUFFER_COPY_ON_WRITE | CX_BUFFER_COPY_ON_EXTEND;
     if (buffer->flags & force_copy_flags) {
         void *newspace = cxMalloc(buffer->allocator, newcap);
@@ -254,42 +216,57 @@
         return 0;
     } else if (cxReallocate(buffer->allocator,
                      (void **) &buffer->bytes, newcap) == 0) {
+        buffer->flags |= CX_BUFFER_FREE_CONTENTS;
         buffer->capacity = newcap;
+        if (buffer->size > newcap) {
+            buffer->size = newcap;
+        }
         return 0;
     } else {
         return -1; // LCOV_EXCL_LINE
     }
 }
 
-static size_t cx_buffer_calculate_minimum_capacity(size_t mincap) {
-    unsigned long pagesize = system_page_size();
-    // if page size is larger than 64 KB - for some reason - truncate to 64 KB
-    if (pagesize > 65536) pagesize = 65536;
-    if (mincap < pagesize) {
-        // when smaller as one page, map to the next power of two
-        mincap--;
-        mincap |= mincap >> 1;
-        mincap |= mincap >> 2;
-        mincap |= mincap >> 4;
-        // last operation only needed for pages larger 4096 bytes
-        // but if/else would be more expensive than just doing this
-        mincap |= mincap >> 8;
-        mincap++;
-    } else {
-        // otherwise, map to a multiple of the page size
-        mincap -= mincap % pagesize;
-        mincap += pagesize;
-        // note: if newcap is already page aligned,
-        // this gives a full additional page (which is good)
+int cxBufferMaximumCapacity(CxBuffer *buffer, size_t capacity) {
+    if (capacity < buffer->capacity) {
+        return -1;
     }
-    return mincap;
+    buffer->max_capacity = capacity;
+    return 0;
 }
 
 int cxBufferMinimumCapacity(CxBuffer *buffer, size_t newcap) {
     if (newcap <= buffer->capacity) {
         return 0;
     }
-    newcap = cx_buffer_calculate_minimum_capacity(newcap);
+    if (newcap > buffer->max_capacity) {
+        return -1;
+    }
+    if (newcap < buffer->max_capacity) {
+        unsigned long pagesize = cx_system_page_size();
+        // if page size is larger than 64 KB - for some reason - truncate to 64 KB
+        if (pagesize > 65536) pagesize = 65536;
+        if (newcap < pagesize) {
+            // when smaller as one page, map to the next power of two
+            newcap--;
+            newcap |= newcap >> 1;
+            newcap |= newcap >> 2;
+            newcap |= newcap >> 4;
+            // last operation only needed for pages larger 4096 bytes
+            // but if/else would be more expensive than just doing this
+            newcap |= newcap >> 8;
+            newcap++;
+        } else {
+            // otherwise, map to a multiple of the page size
+            newcap -= newcap % pagesize;
+            newcap += pagesize;
+            // note: if newcap is already page aligned,
+            // this gives a full additional page (which is good)
+        }
+        if (newcap > buffer->max_capacity) {
+            newcap = buffer->max_capacity;
+        }
+    }
     return cxBufferReserve(buffer, newcap);
 }
 
@@ -315,54 +292,6 @@
     }
 }
 
-static size_t cx_buffer_flush_helper(
-        const CxBuffer *buffer,
-        const unsigned char *src,
-        size_t size,
-        size_t nitems
-) {
-    // flush data from an arbitrary source
-    // does not need to be the buffer's contents
-    size_t max_items = buffer->flush->blksize / size;
-    size_t fblocks = 0;
-    size_t flushed_total = 0;
-    while (nitems > 0 && fblocks < buffer->flush->blkmax) {
-        fblocks++;
-        size_t items = nitems > max_items ? max_items : nitems;
-        size_t flushed = buffer->flush->wfunc(
-            src, size, items, buffer->flush->target);
-        if (flushed > 0) {
-            flushed_total += flushed;
-            src += flushed * size;
-            nitems -= flushed;
-        } else {
-            // if no bytes can be flushed out anymore, we give up
-            break;
-        }
-    }
-    return flushed_total;
-}
-
-static size_t cx_buffer_flush_impl(CxBuffer *buffer, size_t size) {
-    // flush the current contents of the buffer
-    unsigned char *space = buffer->bytes;
-    size_t remaining = buffer->pos / size;
-    size_t flushed_total = cx_buffer_flush_helper(
-        buffer, space, size, remaining);
-
-    // shift the buffer left after flushing
-    // IMPORTANT: up to this point, copy on write must have been
-    // performed already, because we can't do error handling here
-    cxBufferShiftLeft(buffer, flushed_total*size);
-
-    return flushed_total;
-}
-
-size_t cxBufferFlush(CxBuffer *buffer) {
-    if (buffer_copy_on_write(buffer)) return 0;
-    return cx_buffer_flush_impl(buffer, 1);
-}
-
 size_t cxBufferWrite(
         const void *ptr,
         size_t size,
@@ -380,107 +309,52 @@
         return nitems;
     }
 
-    size_t len, total_flushed = 0;
-cx_buffer_write_retry:
+    size_t len;
     if (cx_szmul(size, nitems, &len)) {
         errno = EOVERFLOW;
-        return total_flushed;
+        return 0;
     }
     if (buffer->pos > SIZE_MAX - len) {
         errno = EOVERFLOW;
-        return total_flushed;
+        return 0;
     }
+    const size_t required = buffer->pos + len;
 
-    size_t required = buffer->pos + len;
-    bool perform_flush = false;
+    // check if we need to auto-extend
     if (required > buffer->capacity) {
         if (buffer->flags & CX_BUFFER_AUTO_EXTEND) {
-            if (buffer->flush != NULL) {
-                size_t newcap = cx_buffer_calculate_minimum_capacity(required);
-                if (newcap > buffer->flush->threshold) {
-                    newcap = buffer->flush->threshold;
-                }
-                if (cxBufferReserve(buffer, newcap)) {
-                    return total_flushed; // LCOV_EXCL_LINE
-                }
-                if (required > newcap) {
-                    perform_flush = true;
-                }
-            } else {
-                if (cxBufferMinimumCapacity(buffer, required)) {
-                    return total_flushed; // LCOV_EXCL_LINE
-                }
-            }
-        } else {
-            if (buffer->flush != NULL) {
-                perform_flush = true;
-            } else {
-                // truncate data, if we can neither extend nor flush
-                len = buffer->capacity - buffer->pos;
-                if (size > 1) {
-                    len -= len % size;
-                }
-                nitems = len / size;
+            size_t newcap = required < buffer->max_capacity
+                    ? required : buffer->max_capacity;
+            if (cxBufferMinimumCapacity(buffer, newcap)) {
+                return 0; // LCOV_EXCL_LINE
             }
         }
     }
 
+    // check again and truncate data if capacity is still not enough
+    if (required > buffer->capacity) {
+        len = buffer->capacity - buffer->pos;
+        if (size > 1) {
+            len -= len % size;
+        }
+        nitems = len / size;
+    }
+
     // check here and not above because of possible truncation
     if (len == 0) {
-        return total_flushed;
+        return 0;
     }
 
     // check if we need to copy
     if (buffer_copy_on_write(buffer)) return 0;
 
     // perform the operation
-    if (perform_flush) {
-        size_t items_flushed;
-        if (buffer->pos == 0) {
-            // if we don't have data in the buffer, but are instructed
-            // to flush, it means that we are supposed to relay the data
-            items_flushed = cx_buffer_flush_helper(buffer, ptr, size, nitems);
-            if (items_flushed == 0) {
-                // we needed to relay data, but could not flush anything
-                // i.e. we have to give up to avoid endless trying
-                return 0;
-            }
-            nitems -= items_flushed;
-            total_flushed += items_flushed;
-            if (nitems > 0) {
-                ptr = ((unsigned char*)ptr) + items_flushed * size;
-                goto cx_buffer_write_retry;
-            }
-            return total_flushed;
-        } else {
-            items_flushed = cx_buffer_flush_impl(buffer, size);
-            if (items_flushed == 0) {
-                // flush target is full, let's try to truncate
-                size_t remaining_space;
-                if (buffer->flags & CX_BUFFER_AUTO_EXTEND) {
-                    remaining_space = buffer->flush->threshold > buffer->pos
-                                          ? buffer->flush->threshold - buffer->pos
-                                          : 0;
-                } else {
-                    remaining_space = buffer->capacity > buffer->pos
-                                          ? buffer->capacity - buffer->pos
-                                          : 0;
-                }
-                nitems = remaining_space / size;
-                if (nitems == 0) {
-                    return total_flushed;
-                }
-            }
-            goto cx_buffer_write_retry;
-        }
-    } else {
-        memcpy(buffer->bytes + buffer->pos, ptr, len);
-        buffer->pos += len;
-        if (buffer->pos > buffer->size) {
-            buffer->size = buffer->pos;
-        }
-        return total_flushed + nitems;
+    memcpy(buffer->bytes + buffer->pos, ptr, len);
+    buffer->pos += len;
+    if (buffer->pos > buffer->size) {
+        buffer->size = buffer->pos;
     }
+    return nitems;
 }
 
 size_t cxBufferAppend(
--- a/ucx/cx/allocator.h	Thu Dec 11 22:58:02 2025 +0100
+++ b/ucx/cx/allocator.h	Fri Dec 12 10:42:53 2025 +0100
@@ -146,6 +146,17 @@
                              const CxAllocator *allocator, void *data);
 
 /**
+ * Returns the system's memory page size.
+ *
+ * If the page size cannot be retrieved from the system,
+ * a default of 4096 bytes is assumed.
+ *
+ * @return the system's memory page size in bytes
+ */
+cx_attr_nodiscard
+CX_EXPORT unsigned long cx_system_page_size(void);
+
+/**
  * Reallocate a previously allocated block and changes the pointer in-place,
  * if necessary.
  *
@@ -428,9 +439,9 @@
  */
 #define cxReallocArrayDefault(...) cxReallocArray(cxDefaultAllocator, __VA_ARGS__)
 /**
- * Convenience macro that invokes cxFree() with the cxDefaultAllocator.
+ * Convenience function that invokes cxFree() with the cxDefaultAllocator.
  */
-#define cxFreeDefault(...) cxFree(cxDefaultAllocator, __VA_ARGS__)
+CX_EXPORT void cxFreeDefault(void *mem);
 
 #ifdef __cplusplus
 } // extern "C"
--- a/ucx/cx/buffer.h	Thu Dec 11 22:58:02 2025 +0100
+++ b/ucx/cx/buffer.h	Fri Dec 12 10:42:53 2025 +0100
@@ -99,59 +99,6 @@
  */
 #define cxBufferReadFunc  ((cx_read_func) cxBufferRead)
 
-/**
- * Configuration for automatic flushing.
- */
-struct cx_buffer_flush_config_s {
-    /**
-     * The buffer may not extend beyond this threshold before starting to flush.
-     *
-     * Only used when the buffer uses #CX_BUFFER_AUTO_EXTEND.
-     * The threshold will be the maximum capacity the buffer is extended to
-     * before flushing.
-     */
-    size_t threshold;
-    /**
-     * The block size for the elements to flush.
-     */
-    size_t blksize;
-    /**
-     * The maximum number of blocks to flush in one cycle.
-     *
-     * @attention While it is guaranteed that cxBufferFlush() will not flush
-     * more blocks, this is not necessarily the case for cxBufferWrite().
-     * After performing a flush cycle, cxBufferWrite() will retry the write
-     * operation and potentially trigger another flush cycle, until the
-     * flush target accepts no more data.
-     */
-    size_t blkmax;
-
-    /**
-     * The target for the write function.
-     */
-    void *target;
-
-    /**
-     * The write-function used for flushing.
-     * If NULL, the flushed content gets discarded.
-     */
-    cx_write_func wfunc;
-};
-
-/**
- * Type alias for the flush configuration struct.
- *
- * @code
- * struct cx_buffer_flush_config_s {
- *     size_t threshold;
- *     size_t blksize;
- *     size_t blkmax;
- *     void *target;
- *     cx_write_func wfunc;
- * };
- * @endcode
- */
-typedef struct cx_buffer_flush_config_s CxBufferFlushConfig;
 
 /** Structure for the UCX buffer data. */
 struct cx_buffer_s {
@@ -168,16 +115,12 @@
     };
     /** The allocator to use for automatic memory management. */
     const CxAllocator *allocator;
-    /**
-     * Optional flush configuration
-     *
-     * @see cxBufferEnableFlushing()
-     */
-    CxBufferFlushConfig *flush;
     /** Current position of the buffer. */
     size_t pos;
     /** Current capacity (i.e. maximum size) of the buffer. */
     size_t capacity;
+    /** Maximum capacity that this buffer may grow to. */
+    size_t max_capacity;
     /** Current size of the buffer content. */
     size_t size;
     /**
@@ -231,23 +174,6 @@
         const CxAllocator *allocator, int flags);
 
 /**
- * Configures the buffer for flushing.
- *
- * Flushing can happen automatically when data is written
- * to the buffer (see cxBufferWrite()) or manually when
- * cxBufferFlush() is called.
- *
- * @param buffer the buffer
- * @param config the flush configuration
- * @retval zero success
- * @retval non-zero failure
- * @see cxBufferFlush()
- * @see cxBufferWrite()
- */
-cx_attr_nonnull
-CX_EXPORT int cxBufferEnableFlushing(CxBuffer *buffer, CxBufferFlushConfig config);
-
-/**
  * Destroys the buffer contents.
  *
  * Has no effect if the #CX_BUFFER_FREE_CONTENTS feature is not enabled.
@@ -443,6 +369,8 @@
  * Ensures that the buffer has the required capacity.
  *
  * If the current capacity is not sufficient, the buffer will be extended.
+ * If the current capacity is larger, the buffer is shrunk and superfluous
+ * content is discarded.
  *
  * This function will reserve no more bytes than requested, in contrast to
  * cxBufferMinimumCapacity(), which may reserve more bytes to improve the
@@ -450,7 +378,7 @@
  *
  * @param buffer the buffer
  * @param capacity the required capacity for this buffer
- * @retval zero the capacity was already sufficient or successfully increased
+ * @retval zero on success
  * @retval non-zero on allocation failure
  * @see cxBufferShrink()
  * @see cxBufferMinimumCapacity()
@@ -459,6 +387,25 @@
 CX_EXPORT int cxBufferReserve(CxBuffer *buffer, size_t capacity);
 
 /**
+ * Limits the buffer's capacity.
+ *
+ * If the current capacity is already larger, this function fails and returns
+ * non-zero.
+ *
+ * The capacity limit will affect auto-extension features, as well as future
+ * calls to cxBufferMinimumCapacity() and cxBufferReserve().
+ *
+ * @param buffer the buffer
+ * @param capacity the maximum allowed capacity for this buffer
+ * @retval zero the limit is applied
+ * @retval non-zero the new limit is smaller than the current capacity
+ * @see cxBufferReserve()
+ * @see cxBufferMinimumCapacity()
+ */
+cx_attr_nonnull
+CX_EXPORT int cxBufferMaximumCapacity(CxBuffer *buffer, size_t capacity);
+
+/**
  * Ensures that the buffer has a minimum capacity.
  *
  * If the current capacity is not sufficient, the buffer will be generously
@@ -471,6 +418,7 @@
  * @param capacity the minimum required capacity for this buffer
  * @retval zero the capacity was already sufficient or successfully increased
  * @retval non-zero on allocation failure
+ * @see cxBufferMaximumCapacity()
  * @see cxBufferReserve()
  * @see cxBufferShrink()
  */
@@ -500,33 +448,13 @@
 /**
  * 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
- * by the number of bytes written.
- *
- * If flushing is enabled and the buffer needs to flush, the data is flushed to
- * the target until the target signals that it cannot take more data by
- * returning zero via the respective write function. In that case, the remaining
- * 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
- * buffer are both incapable of taking more data or all data has been written.
+ * If auto-extension is enabled, the buffer's capacity is automatically
+ * increased when it is not large enough to hold all data.
+ * By default, the capacity grows indefinitely, unless limited with
+ * cxBufferMaximumCapacity().
+ * When auto-extension fails, this function writes no data and returns zero.
  *
- * If, after flushing, the number of items that shall be written still exceeds
- * the capacity or flush threshold, this function tries to write all items directly
- * 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.
- * 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 misaligned part in the buffer.
- * Afterward, this function only writes as many items as possible to the buffer.
+ * The position of the buffer is moved alongside the written data.
  *
  * @note The signature is compatible with the fwrite() family of functions.
  *
@@ -566,62 +494,6 @@
         size_t nitems, CxBuffer *buffer);
 
 /**
- * Performs a single flush-run on the specified buffer.
- *
- * Does nothing when the position in the buffer is zero.
- * Otherwise, the data until the current position minus
- * one is considered for flushing.
- * Note carefully that flushing will never exceed the
- * current @em position, even when the size of the
- * buffer is larger than the current position.
- *
- * One flush run will try to flush @c blkmax many
- * blocks of size @c blksize until either the @p buffer
- * has no more data to flush or the write function
- * used for flushing returns zero.
- *
- * The buffer is shifted left for that many bytes
- * the flush operation has successfully flushed.
- *
- * @par Example 1
- * Assume you have a buffer with size 340 and you are
- * 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
- * position are written.
- * 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.
- *
- * @par Example 3
- * Same as Example 1, but now assume the flush target
- * only accepts 100 bytes before returning zero.
- * 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.
- *
- * @remark Just returns zero when flushing was not enabled with
- * cxBufferEnableFlushing().
- *
- * @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 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
- * @return the number of successfully flushed bytes
- * @see cxBufferEnableFlushing()
- */
-cx_attr_nonnull
-CX_EXPORT size_t cxBufferFlush(CxBuffer *buffer);
-
-/**
  * Reads data from a CxBuffer.
  *
  * The position of the buffer is increased by the number of bytes read.
@@ -645,8 +517,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 the buffer extension fails, @c EOF is returned.
+ * the buffer capacity is extended, unless a limit set by
+ * cxBufferMaximumCapacity() is reached.
+ * If the feature is disabled or the buffer extension fails, @c EOF is returned.
  *
  * On successful writing, the position of the buffer is increased.
  *
@@ -655,8 +528,8 @@
  *
  * @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
+ * @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
  * @see cxBufferTerminate()
  */
 cx_attr_nonnull
@@ -665,7 +538,8 @@
 /**
  * Writes a terminating zero to a buffer at the current position.
  *
- * If successful, sets the size to the current position and advances the position by one.
+ * If successful, sets the size to the current position and advances
+ * the position by one.
  *
  * The purpose of this function is to have the written data ready to be used as
  * a C string with the buffer's size being the length of that string.
--- a/ucx/cx/json.h	Thu Dec 11 22:58:02 2025 +0100
+++ b/ucx/cx/json.h	Fri Dec 12 10:42:53 2025 +0100
@@ -41,6 +41,7 @@
 #include "string.h"
 #include "buffer.h"
 #include "array_list.h"
+#include "map.h"
 
 #include <string.h>
 
@@ -188,9 +189,10 @@
  */
 typedef struct cx_json_array_s CxJsonArray;
 /**
- * Type alias for the JSON object struct.
+ * Type alias for the map representing a JSON object.
+ * The map contains pointers of type @c CxJsonValue.
  */
-typedef struct cx_json_object_s CxJsonObject;
+typedef CxMap* CxJsonObject;
 /**
  * Type alias for a JSON string.
  */
@@ -209,46 +211,13 @@
 typedef enum cx_json_literal CxJsonLiteral;
 
 /**
- * Type alias for a key/value pair in a JSON object.
- */
-typedef struct cx_json_obj_value_s CxJsonObjValue;
-
-/**
  * JSON array structure.
  */
 struct cx_json_array_s {
     /**
      * The array data.
      */
-    CX_ARRAY_DECLARE(CxJsonValue*, array);
-};
-
-/**
- * JSON object structure.
- */
-struct cx_json_object_s {
-    /**
-     * The key/value entries.
-     */
-    CX_ARRAY_DECLARE(CxJsonObjValue, values);
-    /**
-     * The original indices to reconstruct the order in which the members were added.
-     */
-    size_t *indices;
-};
-
-/**
- * Structure for a key/value entry in a JSON object.
- */
-struct cx_json_obj_value_s {
-    /**
-     * The key (or name in JSON terminology) of the value.
-     */
-    cxmutstr name;
-    /**
-     * The value.
-     */
-    CxJsonValue *value;
+    CX_ARRAY_DECLARE(CxJsonValue*, data);
 };
 
 /**
@@ -295,7 +264,7 @@
          * The literal type if the type is #CX_JSON_LITERAL.
          */
         CxJsonLiteral literal;
-    } value;
+    };
 };
 
 /**
@@ -349,11 +318,11 @@
     CxJsonValue *parsed;
 
     /**
-     * A pointer to an intermediate state of a currently parsed object member.
+     * The name of a not yet completely parsed object member.
      *
      * Never access this value manually.
      */
-    CxJsonObjValue uncompleted_member;
+    cxmutstr uncompleted_member_name;
 
     /**
      * State stack.
@@ -439,10 +408,6 @@
      */
     bool pretty;
     /**
-     * Set false to output the members in the order in which they were added.
-     */
-    bool sort_members;
-    /**
      * 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.
@@ -530,8 +495,8 @@
 /**
  * Destroys and re-initializes the JSON interface.
  *
- * You might want to use this to reset the parser after
- * encountering a syntax error.
+ * You must use this to reset the parser after encountering a syntax error
+ * if you want to continue using it.
  *
  * @param json the JSON interface
  */
@@ -592,6 +557,36 @@
  */
 #define cxJsonFill(json, str) cx_json_fill(json, cx_strcast(str))
 
+
+/**
+ * Internal function - use cxJsonFromString() instead.
+ *
+ * @param allocator the allocator for the JSON value
+ * @param str the string to parse
+ * @param value a pointer where the JSON value shall be stored to
+ * @return status code
+ */
+cx_attr_nonnull_arg(3)
+CX_EXPORT CxJsonStatus cx_json_from_string(const CxAllocator *allocator,
+            cxstring str, CxJsonValue **value);
+
+/**
+ * Parses a string into a JSON value.
+ *
+ * @param allocator (@c CxAllocator*) the allocator for the JSON value
+ * @param str (any string) the string to parse
+ * @param value (@c CxJsonValue**) a pointer where the JSON value shall be stored to
+ * @retval CX_JSON_NO_ERROR success
+ * @retval CX_JSON_NO_DATA the string was empty or blank
+ * @retval CX_JSON_INCOMPLETE_DATA the string unexpectedly ended
+ * @retval CX_JSON_BUFFER_ALLOC_FAILED allocating internal buffer space failed
+ * @retval CX_JSON_VALUE_ALLOC_FAILED allocating memory for the CxJsonValue failed
+ * @retval CX_JSON_FORMAT_ERROR_NUMBER the JSON text contains an illegally formatted number
+ * @retval CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN JSON syntax error
+ */
+#define cxJsonFromString(allocator, str, value) \
+        cx_json_from_string(allocator, cx_strcast(str), value)
+
 /**
  * Creates a new (empty) JSON object.
  *
@@ -1077,7 +1072,7 @@
  */
 cx_attr_nonnull
 CX_INLINE bool cxJsonIsBool(const CxJsonValue *value) {
-    return cxJsonIsLiteral(value) && value->value.literal != CX_JSON_NULL;
+    return cxJsonIsLiteral(value) && value->literal != CX_JSON_NULL;
 }
 
 /**
@@ -1094,7 +1089,7 @@
  */
 cx_attr_nonnull
 CX_INLINE bool cxJsonIsTrue(const CxJsonValue *value) {
-    return cxJsonIsLiteral(value) && value->value.literal == CX_JSON_TRUE;
+    return cxJsonIsLiteral(value) && value->literal == CX_JSON_TRUE;
 }
 
 /**
@@ -1111,7 +1106,7 @@
  */
 cx_attr_nonnull
 CX_INLINE bool cxJsonIsFalse(const CxJsonValue *value) {
-    return cxJsonIsLiteral(value) && value->value.literal == CX_JSON_FALSE;
+    return cxJsonIsLiteral(value) && value->literal == CX_JSON_FALSE;
 }
 
 /**
@@ -1124,7 +1119,7 @@
  */
 cx_attr_nonnull
 CX_INLINE bool cxJsonIsNull(const CxJsonValue *value) {
-    return cxJsonIsLiteral(value) && value->value.literal == CX_JSON_NULL;
+    return cxJsonIsLiteral(value) && value->literal == CX_JSON_NULL;
 }
 
 /**
@@ -1202,7 +1197,7 @@
  */
 cx_attr_nonnull
 CX_INLINE bool cxJsonAsBool(const CxJsonValue *value) {
-    return value->value.literal == CX_JSON_TRUE;
+    return value->literal == CX_JSON_TRUE;
 }
 
 /**
@@ -1216,7 +1211,7 @@
  */
 cx_attr_nonnull
 CX_INLINE size_t cxJsonArrSize(const CxJsonValue *value) {
-    return value->value.array.array_size;
+    return value->array.data_size;
 }
 
 /**
@@ -1277,14 +1272,14 @@
  */
 cx_attr_nonnull
 CX_INLINE size_t cxJsonObjSize(const CxJsonValue *value) {
-    return value->value.object.values_size;
+    return cxCollectionSize(value->object);
 }
 
 /**
- * Returns an iterator over the JSON object members.
+ * Returns a map iterator over the JSON object members.
  *
- * The iterator yields values of type @c CxJsonObjValue* which
- * contain the name and value of the member.
+ * The iterator yields values of type @c CxMapEntry* which
+ * contain the name and the @c CxJsonObjValue* of the member.
  *
  * If the @p value is not a JSON object, the behavior is undefined.
  *
@@ -1293,7 +1288,7 @@
  * @see cxJsonIsObject()
  */
 cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT CxIterator cxJsonObjIter(const CxJsonValue *value);
+CX_EXPORT CxMapIterator cxJsonObjIter(const CxJsonValue *value);
 
 /**
  * Internal function, do not use.
--- a/ucx/cx/properties.h	Thu Dec 11 22:58:02 2025 +0100
+++ b/ucx/cx/properties.h	Fri Dec 12 10:42:53 2025 +0100
@@ -41,9 +41,6 @@
 #include "map.h"
 #include "buffer.h"
 
-#include <stdio.h>
-#include <string.h>
-
 #ifdef __cplusplus
 extern "C" {
 #endif
@@ -80,9 +77,6 @@
      * The character, when appearing at the end of a line, continues that line.
      * This is '\' by default.
      */
-    /**
-     * Reserved for future use.
-     */
     char continuation;
 };
 
@@ -141,23 +135,16 @@
      */
     CX_PROPERTIES_BUFFER_ALLOC_FAILED,
     /**
-     * Initializing the properties source failed.
-     *
-     * @see cx_properties_read_init_func
+     * A file operation failed.
+     * Only for cxPropertiesLoad().
+     * It is system-specific if errno is set.
      */
-    CX_PROPERTIES_READ_INIT_FAILED,
+    CX_PROPERTIES_FILE_ERROR,
     /**
-     * Reading from a properties source failed.
-     *
-     * @see cx_properties_read_func
+     * A map operation failed.
+     * Only for cxPropertiesLoad().
      */
-    CX_PROPERTIES_READ_FAILED,
-    /**
-     * Sinking a k/v-pair failed.
-     *
-     * @see cx_properties_sink_func
-     */
-    CX_PROPERTIES_SINK_FAILED,
+    CX_PROPERTIES_MAP_ERROR,
 };
 
 /**
@@ -190,134 +177,6 @@
  */
 typedef struct cx_properties_s CxProperties;
 
-
-/**
- * Typedef for a properties sink.
- */
-typedef struct cx_properties_sink_s CxPropertiesSink;
-
-/**
- * A function that consumes a k/v-pair in a sink.
- *
- * 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
- * @param sink the sink
- * @param key the key
- * @param value the value
- * @retval zero success
- * @retval non-zero sinking the k/v-pair failed
- */
-typedef int(*cx_properties_sink_func)(
-        CxProperties *prop,
-        CxPropertiesSink *sink,
-        cxstring key,
-        cxstring value
-);
-
-/**
- * Defines a sink for k/v-pairs.
- */
-struct cx_properties_sink_s {
-    /**
-     * The sink object.
-     */
-    void *sink;
-    /**
-     * Optional custom data.
-     */
-    void *data;
-    /**
-     * A function for consuming k/v-pairs into the sink.
-     */
-    cx_properties_sink_func sink_func;
-};
-
-
-/**
- * Typedef for a properties source.
- */
-typedef struct cx_properties_source_s CxPropertiesSource;
-
-/**
- * A function that reads data from a source.
- *
- * When the source is depleted, implementations SHALL provide an empty
- * string in the @p target and return zero.
- * A non-zero return value is only permitted in case of an error.
- *
- * The meaning of the optional parameters is implementation-dependent.
- *
- * @param prop the properties interface that wants to read from the source
- * @param src the source
- * @param target a string buffer where the read data shall be stored
- * @retval zero success
- * @retval non-zero reading the data failed
- */
-typedef int(*cx_properties_read_func)(
-        CxProperties *prop,
-        CxPropertiesSource *src,
-        cxstring *target
-);
-
-/**
- * A function that may initialize additional memory for the source.
- *
- * @param prop the properties interface that wants to read from the source
- * @param src the source
- * @retval zero initialization was successful
- * @retval non-zero otherwise
- */
-typedef int(*cx_properties_read_init_func)(
-        CxProperties *prop,
-        CxPropertiesSource *src
-);
-
-/**
- * A function that cleans memory initialized by the read_init_func.
- *
- * @param prop the properties interface that wants to read from the source
- * @param src the source
- */
-typedef void(*cx_properties_read_clean_func)(
-        CxProperties *prop,
-        CxPropertiesSource *src
-);
-
-/**
- * Defines a properties source.
- */
-struct cx_properties_source_s {
-    /**
-     * The source object.
-     *
-     * For example, a file stream or a string.
-     */
-    void *src;
-    /**
-     * Optional additional data pointer.
-     */
-    void *data_ptr;
-    /**
-     * Optional size information.
-     */
-    size_t data_size;
-    /**
-     * A function that reads data from the source.
-     */
-    cx_properties_read_func read_func;
-    /**
-     * Optional function that may prepare the source for reading data.
-     */
-    cx_properties_read_init_func read_init_func;
-    /**
-     * Optional function that cleans additional memory allocated by the
-     * read_init_func.
-     */
-    cx_properties_read_clean_func read_clean_func;
-};
-
 /**
  * Initialize a properties interface.
  *
@@ -465,100 +324,83 @@
 CX_EXPORT CxPropertiesStatus cxPropertiesNext(CxProperties *prop, cxstring *key, cxstring *value);
 
 /**
- * Creates a properties sink for an UCX map.
- *
- * The values stored in the map will be pointers to freshly allocated,
- * zero-terminated C strings (@c char*), which means the @p map should have been
- * created with #CX_STORE_POINTERS.
- *
- * The cxDefaultAllocator will be used unless you specify a custom
- * allocator in the optional @c data field of the returned sink.
- *
- * @param map the map that shall consume the k/v-pairs.
- * @return the sink
- * @see cxPropertiesLoad()
+ * The size of the stack memory that `cxPropertiesLoad()` will reserve with `cxPropertiesUseStack()`.
  */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT CxPropertiesSink cxPropertiesMapSink(CxMap *map);
+CX_EXPORT extern const unsigned cx_properties_load_buf_size;
 
 /**
- * Creates a properties source based on an UCX string.
- *
- * @param str the string
- * @return the properties source
- * @see cxPropertiesLoad()
+ * The size of the stack memory that `cxPropertiesLoad()` will use to read contents from the file.
  */
-cx_attr_nodiscard
-CX_EXPORT CxPropertiesSource cxPropertiesStringSource(cxstring str);
-
-/**
- * Creates a properties source based on C string with the specified length.
- *
- * @param str the string
- * @param len the length
- * @return the properties source
- * @see cxPropertiesLoad()
- */
-cx_attr_nonnull cx_attr_nodiscard cx_attr_access_r(1, 2)
-CX_EXPORT CxPropertiesSource cxPropertiesCstrnSource(const char *str, size_t len);
+CX_EXPORT extern const unsigned cx_properties_load_fill_size;
 
 /**
- * Creates a properties source based on a C string.
- *
- * The length will be determined with strlen(), so the string MUST be
- * zero-terminated.
+ * Internal function - use cxPropertiesLoad() instead.
  *
- * @param str the string
- * @return the properties source
- * @see cxPropertiesLoad()
+ * @param config the parser config
+ * @param allocator the allocator for the values
+ * @param filename the file name
+ * @param target the target map
+ * @return status code
  */
-cx_attr_nonnull cx_attr_nodiscard cx_attr_cstr_arg(1)
-CX_EXPORT CxPropertiesSource cxPropertiesCstrSource(const char *str);
+cx_attr_nonnull_arg(4)
+CX_EXPORT CxPropertiesStatus cx_properties_load(CxPropertiesConfig config,
+        const CxAllocator *allocator, cxstring filename, CxMap *target);
 
 /**
- * Creates a properties source based on an FILE.
- *
- * @param file the file
- * @param chunk_size how many bytes may be read in one operation
+ * Loads properties from a file and inserts them into a map.
  *
- * @return the properties source
- * @see cxPropertiesLoad()
- */
-cx_attr_nonnull cx_attr_nodiscard cx_attr_access_r(1)
-CX_EXPORT CxPropertiesSource cxPropertiesFileSource(FILE *file, size_t chunk_size);
-
-
-/**
- * Loads properties data from a source and transfers it to a sink.
+ * Entries are added to the map, possibly overwriting existing entries.
+ *
+ * The map must either store pointers of type @c char*, or elements of type cxmutstr.
+ * Any other configuration is not supported.
  *
- * This function tries to read as much data from the source as possible.
- * When the source was completely consumed and at least on k/v-pair was found,
- * the return value will be #CX_PROPERTIES_NO_ERROR.
- * When the source was consumed but no k/v-pairs were found, the return value
- * will be #CX_PROPERTIES_NO_DATA.
- * In case the source data ends unexpectedly, the #CX_PROPERTIES_INCOMPLETE_DATA
- * is returned. In that case you should call this function again with the same
- * sink and either an updated source or the same source if the source is able to
- * yield the missing data.
+ * @note When the parser finds an error, all successfully parsed keys before the error
+ * are added to the map nonetheless.
  *
- * The other result codes apply, according to their description.
- *
- * @param prop the properties interface
- * @param sink the sink
- * @param source the source
- * @retval CX_PROPERTIES_NO_ERROR (zero) a key/value pair was found
- * @retval CX_PROPERTIES_READ_INIT_FAILED initializing the source failed
- * @retval CX_PROPERTIES_READ_FAILED reading from the source failed
- * @retval CX_PROPERTIES_SINK_FAILED sinking the properties into the sink failed
- * @retval CX_PROPERTIES_NO_DATA the source did not provide any key/value pairs
- * @retval CX_PROPERTIES_INCOMPLETE_DATA the source did not provide enough data
+ * @param config the parser config
+ * @param allocator the allocator for the values that will be stored in the map
+ * @param filename (any string) the absolute or relative path to the file
+ * @param target (@c CxMap*) the map where the properties shall be added
+ * @retval CX_PROPERTIES_NO_ERROR (zero) at least one key/value pair was found
+ * @retval CX_PROPERTIES_NO_DATA the file is syntactically OK, but does not contain properties
+ * @retval CX_PROPERTIES_INCOMPLETE_DATA unexpected end of file
  * @retval CX_PROPERTIES_INVALID_EMPTY_KEY the properties data contains an illegal empty key
  * @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
+ * @retval CX_PROPERTIES_FILE_ERROR a file operation failed; depending on the system @c errno might be set
+ * @retval CX_PROPERTIES_MAP_ERROR storing a key/value pair in the map failed
+ * @see cxPropertiesLoadDefault()
  */
-cx_attr_nonnull
-CX_EXPORT CxPropertiesStatus cxPropertiesLoad(CxProperties *prop,
-        CxPropertiesSink sink, CxPropertiesSource source);
+#define cxPropertiesLoad(config, allocator, filename, target) \
+    cx_properties_load(config, allocator, cx_strcast(filename), target)
+
+/**
+ * Loads properties from a file and inserts them into a map with a default config.
+ *
+ * Entries are added to the map, possibly overwriting existing entries.
+ *
+ * The map must either store pointers of type @c char*, or elements of type cxmutstr.
+ * Any other configuration is not supported.
+ *
+ * @note When the parser finds an error, all successfully parsed keys before the error
+ * are added to the map nonetheless.
+ *
+ * @param allocator the allocator for the values that will be stored in the map
+ * @param filename (any string) the absolute or relative path to the file
+ * @param target (@c CxMap*) the map where the properties shall be added
+ * @retval CX_PROPERTIES_NO_ERROR (zero) at least one key/value pair was found
+ * @retval CX_PROPERTIES_NO_DATA the file is syntactically OK, but does not contain properties
+ * @retval CX_PROPERTIES_INCOMPLETE_DATA unexpected end of file
+ * @retval CX_PROPERTIES_INVALID_EMPTY_KEY the properties data contains an illegal empty key
+ * @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
+ * @retval CX_PROPERTIES_FILE_ERROR a file operation failed; depending on the system @c errno might be set
+ * @retval CX_PROPERTIES_MAP_ERROR storing a key/value pair in the map failed
+ * @see cxPropertiesLoad()
+ */
+#define cxPropertiesLoadDefault(allocator, filename, target) \
+    cx_properties_load(cx_properties_config_default, allocator, cx_strcast(filename), target)
+
 
 #ifdef __cplusplus
 } // extern "C"
--- a/ucx/cx/tree.h	Thu Dec 11 22:58:02 2025 +0100
+++ b/ucx/cx/tree.h	Fri Dec 12 10:42:53 2025 +0100
@@ -643,17 +643,13 @@
  * Structure for holding the base data of a tree.
  */
 struct cx_tree_s {
+    CX_COLLECTION_BASE;
     /**
      * The tree class definition.
      */
     const cx_tree_class *cl;
 
     /**
-     * Allocator to allocate new nodes.
-     */
-    const CxAllocator *allocator;
-
-    /**
      * A pointer to the root node.
      *
      * Will be @c NULL when @c size is 0.
@@ -671,21 +667,6 @@
     cx_tree_node_create_func node_create;
 
     /**
-     * An optional simple destructor for the tree nodes.
-     */
-    cx_destructor_func simple_destructor;
-
-    /**
-     * An optional advanced destructor for the tree nodes.
-     */
-    cx_destructor_func2 advanced_destructor;
-
-    /**
-     * The pointer to additional data that is passed to the advanced destructor.
-     */
-    void *destructor_data;
-
-    /**
      * A function to compare two nodes.
      */
     cx_tree_search_func search;
@@ -696,11 +677,6 @@
     cx_tree_search_data_func search_data;
 
     /**
-     * The number of currently stored elements.
-     */
-    size_t size;
-
-    /**
      * Offset in the node struct for the parent pointer.
      */
     ptrdiff_t loc_parent;
--- a/ucx/json.c	Thu Dec 11 22:58:02 2025 +0100
+++ b/ucx/json.c	Fri Dec 12 10:42:53 2025 +0100
@@ -27,6 +27,7 @@
  */
 
 #include "cx/json.h"
+#include "cx/kv_list.h"
 
 #include <string.h>
 #include <assert.h>
@@ -41,90 +42,10 @@
 
 static CxJsonValue cx_json_value_nothing = {.type = CX_JSON_NOTHING};
 
-static int json_cmp_objvalue(const void *l, const void *r) {
-    const CxJsonObjValue *left = l;
-    const CxJsonObjValue *right = r;
-    return cx_strcmp(cx_strcast(left->name), cx_strcast(right->name));
-}
-
-static size_t json_find_objvalue(const CxJsonValue *obj, cxstring name) {
-    assert(obj->type == CX_JSON_OBJECT);
-    CxJsonObjValue kv_dummy;
-    kv_dummy.name = cx_mutstrn((char*) name.ptr, name.length);
-    return cx_array_binary_search(
-            obj->value.object.values,
-            obj->value.object.values_size,
-            sizeof(CxJsonObjValue),
-            &kv_dummy,
-            json_cmp_objvalue
-    );
-}
-
-static int json_add_objvalue(CxJsonValue *objv, CxJsonObjValue member) {
-    assert(objv->type == CX_JSON_OBJECT);
-    const CxAllocator * const al = objv->allocator;
-    CxJsonObject *obj = &(objv->value.object);
-
-    // determine the index where we need to insert the new member
-    size_t index = cx_array_binary_search_sup(
-        obj->values,
-        obj->values_size,
-        sizeof(CxJsonObjValue),
-        &member, json_cmp_objvalue
-    );
-
-    // is the name already present?
-    if (index < obj->values_size && 0 == json_cmp_objvalue(&member, &obj->values[index])) {
-        // free the original value
-        cx_strfree_a(al, &obj->values[index].name);
-        cxJsonValueFree(obj->values[index].value);
-        // replace the item
-        obj->values[index] = member;
-
-        // nothing more to do
-        return 0;
-    }
-
-    // determine the old capacity and reserve for one more element
-    CxArrayReallocator arealloc = cx_array_reallocator(al, NULL);
-    size_t oldcap = obj->values_capacity;
-    if (cx_array_simple_reserve_a(&arealloc, obj->values, 1)) return 1;
-
-    // check the new capacity, if we need to realloc the index array
-    size_t newcap = obj->values_capacity;
-    if (newcap > oldcap) {
-        if (cxReallocateArray(al, &obj->indices, newcap, sizeof(size_t))) {
-            return 1; // LCOV_EXCL_LINE
-        }
-    }
-
-    // check if append or insert
-    if (index < obj->values_size) {
-        // move the other elements
-        memmove(
-            &obj->values[index+1],
-            &obj->values[index],
-            (obj->values_size - index) * sizeof(CxJsonObjValue)
-        );
-        // increase indices for the moved elements
-        for (size_t i = 0; i < obj->values_size ; i++) {
-            if (obj->indices[i] >= index) {
-                obj->indices[i]++;
-            }
-        }
-    }
-
-    // insert the element and set the index
-    obj->values[index] = member;
-    obj->indices[obj->values_size] = index;
-    obj->values_size++;
-
-    return 0;
-}
-
 static void token_destroy(CxJsonToken *token) {
     if (token->allocated) {
         cx_strfree(&token->content);
+        token->allocated = false;
     }
 }
 
@@ -307,7 +228,9 @@
         }
     }
 
-    if (ttype != CX_JSON_NO_TOKEN) {
+    if (ttype == CX_JSON_NO_TOKEN) {
+        return CX_JSON_NO_DATA;
+    } else {
         // uncompleted token
         size_t uncompleted_len = json->buffer.size - token_part_start;
         if (json->uncompleted.tokentype == CX_JSON_NO_TOKEN) {
@@ -334,9 +257,8 @@
         }
         // advance the buffer position - we saved the stuff in the uncompleted token
         json->buffer.pos += uncompleted_len;
+        return CX_JSON_INCOMPLETE_DATA;
     }
-
-    return CX_JSON_INCOMPLETE_DATA;
 }
 
 // converts a Unicode codepoint to utf8
@@ -473,7 +395,7 @@
     return result;
 }
 
-static cxmutstr escape_string(cxmutstr str, bool escape_slash) {
+static cxmutstr escape_string(cxstring str, bool escape_slash) {
     // note: this function produces the string without enclosing quotes
     // the reason is that we don't want to allocate memory just for that
     CxBuffer buf = {0};
@@ -519,11 +441,27 @@
             cxBufferPut(&buf, c);
         }
     }
-    if (!all_printable) {
-        str = cx_mutstrn(buf.space, buf.size);
+    cxmutstr ret;
+    if (all_printable) {
+        // don't copy the string when we don't need to escape anything
+        ret = cx_mutstrn((char*)str.ptr, str.length);
+    } else {
+        ret = cx_mutstrn(buf.space, buf.size);
     }
     cxBufferDestroy(&buf);
-    return str;
+    return ret;
+}
+
+static CxJsonObject json_create_object_map(const CxAllocator *allocator) {
+    // TODO: we might want to add a comparator that is sorting the elements by their key
+    CxMap *map = cxKvListCreateAsMap(allocator, NULL, CX_STORE_POINTERS);
+    if (map == NULL) return NULL; // LCOV_EXCL_LINE
+    cxDefineDestructor(map, cxJsonValueFree);
+    return map;
+}
+
+static void json_free_object_map(CxJsonObject obj) {
+    cxMapFree(obj);
 }
 
 static CxJsonValue* json_create_value(CxJson *json, CxJsonValueType type) {
@@ -534,14 +472,11 @@
     v->type = type;
     v->allocator = json->allocator;
     if (type == CX_JSON_ARRAY) {
-        cx_array_initialize_a(json->allocator, v->value.array.array, 16);
-        if (v->value.array.array == NULL) goto create_json_value_exit_error; // LCOV_EXCL_LINE
+        cx_array_initialize_a(json->allocator, v->array.data, 16);
+        if (v->array.data == NULL) goto create_json_value_exit_error; // LCOV_EXCL_LINE
     } else if (type == CX_JSON_OBJECT) {
-        cx_array_initialize_a(json->allocator, v->value.object.values, 16);
-        v->value.object.indices = cxCalloc(json->allocator, 16, sizeof(size_t));
-        if (v->value.object.values == NULL ||
-            v->value.object.indices == NULL)
-            goto create_json_value_exit_error; // LCOV_EXCL_LINE
+        v->object = json_create_object_map(json->allocator);
+        if (v->object == NULL) goto create_json_value_exit_error; // LCOV_EXCL_LINE
     }
 
     // add the new value to a possible parent
@@ -550,17 +485,17 @@
         assert(parent != NULL);
         if (parent->type == CX_JSON_ARRAY) {
             CxArrayReallocator value_realloc = cx_array_reallocator(json->allocator, NULL);
-            if (cx_array_simple_add_a(&value_realloc, parent->value.array.array, v)) {
+            if (cx_array_simple_add_a(&value_realloc, parent->array.data, v)) {
                 goto create_json_value_exit_error; // LCOV_EXCL_LINE
             }
         } else if (parent->type == CX_JSON_OBJECT) {
             // the member was already created after parsing the name
-            assert(json->uncompleted_member.name.ptr != NULL);
-            json->uncompleted_member.value = v;
-            if (json_add_objvalue(parent, json->uncompleted_member))  {
+            // store the pointer of the uncompleted value in the map
+            assert(json->uncompleted_member_name.ptr != NULL);
+            if (cxMapPut(parent->object, json->uncompleted_member_name, v)) {
                 goto create_json_value_exit_error; // LCOV_EXCL_LINE
             }
-            json->uncompleted_member.name = (cxmutstr) {NULL, 0};
+            cx_strfree_a(json->allocator, &json->uncompleted_member_name);
         } else {
             assert(false); // LCOV_EXCL_LINE
         }
@@ -624,10 +559,8 @@
     }
     cxJsonValueFree(json->parsed);
     json->parsed = NULL;
-    if (json->uncompleted_member.name.ptr != NULL) {
-        cx_strfree_a(json->allocator, &json->uncompleted_member.name);
-        json->uncompleted_member = (CxJsonObjValue){{NULL, 0}, NULL};
-    }
+    token_destroy(&json->uncompleted);
+    cx_strfree_a(json->allocator, &json->uncompleted_member_name);
 }
 
 void cxJsonReset(CxJson *json) {
@@ -720,7 +653,7 @@
                 if (str.ptr == NULL) {
                     return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE
                 }
-                vbuf->value.string = str;
+                vbuf->string = str;
                 return_rec(CX_JSON_NO_ERROR);
             }
             case CX_JSON_TOKEN_INTEGER:
@@ -730,11 +663,11 @@
                     return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE
                 }
                 if (type == CX_JSON_INTEGER) {
-                    if (cx_strtoi64(token.content, &vbuf->value.integer, 10)) {
+                    if (cx_strtoi64(token.content, &vbuf->integer, 10)) {
                         return_rec(CX_JSON_FORMAT_ERROR_NUMBER);
                     }
                 } else {
-                    if (cx_strtod(token.content, &vbuf->value.number)) {
+                    if (cx_strtod(token.content, &vbuf->number)) {
                         // TODO: at the moment this is unreachable, because the tokenizer is already stricter than cx_strtod()
                         return_rec(CX_JSON_FORMAT_ERROR_NUMBER);  // LCOV_EXCL_LINE
                     }
@@ -746,11 +679,11 @@
                     return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE
                 }
                 if (0 == cx_strcmp(cx_strcast(token.content), cx_str("true"))) {
-                    vbuf->value.literal = CX_JSON_TRUE;
+                    vbuf->literal = CX_JSON_TRUE;
                 } else if (0 == cx_strcmp(cx_strcast(token.content), cx_str("false"))) {
-                    vbuf->value.literal = CX_JSON_FALSE;
+                    vbuf->literal = CX_JSON_FALSE;
                 } else {
-                    vbuf->value.literal = CX_JSON_NULL;
+                    vbuf->literal = CX_JSON_NULL;
                 }
                 return_rec(CX_JSON_NO_ERROR);
             }
@@ -786,8 +719,8 @@
             if (name.ptr == NULL) {
                 return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE
             }
-            assert(json->uncompleted_member.name.ptr == NULL);
-            json->uncompleted_member.name = name;
+            assert(json->uncompleted_member_name.ptr == NULL);
+            json->uncompleted_member_name = name;
             assert(json->vbuf_size > 0);
 
             // next state
@@ -860,29 +793,54 @@
     return result;
 }
 
+CxJsonStatus cx_json_from_string(const CxAllocator *allocator,
+            cxstring str, CxJsonValue **value) {
+    *value = &cx_json_value_nothing;
+    CxJson parser;
+    cxJsonInit(&parser, allocator);
+    if (cxJsonFill(&parser, str)) {
+        // LCOV_EXCL_START
+        cxJsonDestroy(&parser);
+        return CX_JSON_BUFFER_ALLOC_FAILED;
+        // LCOV_EXCL_STOP
+    }
+    CxJsonStatus status = cxJsonNext(&parser, value);
+    // check if we consume the total string
+    CxJsonValue *chk_value = NULL;
+    CxJsonStatus chk_status = CX_JSON_NO_DATA;
+    if (status == CX_JSON_NO_ERROR) {
+        chk_status = cxJsonNext(&parser, &chk_value);
+    }
+    cxJsonDestroy(&parser);
+    if (chk_status == CX_JSON_NO_DATA) {
+        return status;
+    } else {
+        cxJsonValueFree(*value);
+        // if chk_value is nothing, the free is harmless
+        cxJsonValueFree(chk_value);
+        *value = &cx_json_value_nothing;
+        return CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN;
+    }
+
+}
+
 void cxJsonValueFree(CxJsonValue *value) {
     if (value == NULL || value->type == CX_JSON_NOTHING) return;
     switch (value->type) {
         case CX_JSON_OBJECT: {
-            CxJsonObject obj = value->value.object;
-            for (size_t i = 0; i < obj.values_size; i++) {
-                cxJsonValueFree(obj.values[i].value);
-                cx_strfree_a(value->allocator, &obj.values[i].name);
-            }
-            cxFree(value->allocator, obj.values);
-            cxFree(value->allocator, obj.indices);
+            json_free_object_map(value->object);
             break;
         }
         case CX_JSON_ARRAY: {
-            CxJsonArray array = value->value.array;
-            for (size_t i = 0; i < array.array_size; i++) {
-                cxJsonValueFree(array.array[i]);
+            CxJsonArray array = value->array;
+            for (size_t i = 0; i < array.data_size; i++) {
+                cxJsonValueFree(array.data[i]);
             }
-            cxFree(value->allocator, array.array);
+            cxFree(value->allocator, array.data);
             break;
         }
         case CX_JSON_STRING: {
-            cxFree(value->allocator, value->value.string.ptr);
+            cxFree(value->allocator, value->string.ptr);
             break;
         }
         default: {
@@ -898,15 +856,8 @@
     if (v == NULL) return NULL;
     v->allocator = allocator;
     v->type = CX_JSON_OBJECT;
-    cx_array_initialize_a(allocator, v->value.object.values, 16);
-    if (v->value.object.values == NULL) { // LCOV_EXCL_START
-        cxFree(allocator, v);
-        return NULL;
-        // LCOV_EXCL_STOP
-    }
-    v->value.object.indices = cxCalloc(allocator, 16, sizeof(size_t));
-    if (v->value.object.indices == NULL) { // LCOV_EXCL_START
-        cxFree(allocator, v->value.object.values);
+    v->object = json_create_object_map(allocator);
+    if (v->object == NULL) { // LCOV_EXCL_START
         cxFree(allocator, v);
         return NULL;
         // LCOV_EXCL_STOP
@@ -920,8 +871,8 @@
     if (v == NULL) return NULL;
     v->allocator = allocator;
     v->type = CX_JSON_ARRAY;
-    cx_array_initialize_a(allocator, v->value.array.array, 16);
-    if (v->value.array.array == NULL) { cxFree(allocator, v); return NULL; }
+    cx_array_initialize_a(allocator, v->array.data, 16);
+    if (v->array.data == NULL) { cxFree(allocator, v); return NULL; }
     return v;
 }
 
@@ -931,7 +882,7 @@
     if (v == NULL) return NULL;
     v->allocator = allocator;
     v->type = CX_JSON_NUMBER;
-    v->value.number = num;
+    v->number = num;
     return v;
 }
 
@@ -941,7 +892,7 @@
     if (v == NULL) return NULL;
     v->allocator = allocator;
     v->type = CX_JSON_INTEGER;
-    v->value.integer = num;
+    v->integer = num;
     return v;
 }
 
@@ -953,7 +904,7 @@
     v->type = CX_JSON_STRING;
     cxmutstr s = cx_strdup_a(allocator, str);
     if (s.ptr == NULL) { cxFree(allocator, v); return NULL; }
-    v->value.string = s;
+    v->string = s;
     return v;
 }
 
@@ -963,7 +914,7 @@
     if (v == NULL) return NULL;
     v->allocator = allocator;
     v->type = CX_JSON_LITERAL;
-    v->value.literal = lit;
+    v->literal = lit;
     return v;
 }
 
@@ -1042,24 +993,14 @@
     CxArrayReallocator value_realloc = cx_array_reallocator(arr->allocator, NULL);
     assert(arr->type == CX_JSON_ARRAY);
     return cx_array_simple_copy_a(&value_realloc,
-            arr->value.array.array,
-            arr->value.array.array_size,
+            arr->array.data,
+            arr->array.data_size,
             val, count
     );
 }
 
 int cx_json_obj_put(CxJsonValue* obj, cxstring name, CxJsonValue* child) {
-    cxmutstr k = cx_strdup_a(obj->allocator, name);
-    if (k.ptr == NULL) return -1;
-    CxJsonObjValue kv = {k, child};
-    if (json_add_objvalue(obj, kv)) {
-        // LCOV_EXCL_START
-        cx_strfree_a(obj->allocator, &k);
-        return 1;
-        // LCOV_EXCL_STOP
-    } else {
-        return 0;
-    }
+    return cxMapPut(obj->object, name, child);
 }
 
 CxJsonValue* cx_json_obj_put_obj(CxJsonValue* obj, cxstring name) {
@@ -1105,98 +1046,84 @@
 }
 
 CxJsonValue *cxJsonArrGet(const CxJsonValue *value, size_t index) {
-    if (index >= value->value.array.array_size) {
+    if (index >= value->array.data_size) {
         return &cx_json_value_nothing;
     }
-    return value->value.array.array[index];
+    return value->array.data[index];
 }
 
 CxJsonValue *cxJsonArrRemove(CxJsonValue *value, size_t index) {
-    if (index >= value->value.array.array_size) {
+    if (index >= value->array.data_size) {
         return NULL;
     }
-    CxJsonValue *ret = value->value.array.array[index];
+    CxJsonValue *ret = value->array.data[index];
     // TODO: replace with a low level cx_array_remove()
-    size_t count = value->value.array.array_size - index - 1;
+    size_t count = value->array.data_size - index - 1;
     if (count > 0) {
-        memmove(value->value.array.array + index, value->value.array.array + index + 1, count * sizeof(CxJsonValue*));
+        memmove(value->array.data + index, value->array.data + index + 1, count * sizeof(CxJsonValue*));
     }
-    value->value.array.array_size--;
+    value->array.data_size--;
     return ret;
 }
 
 char *cxJsonAsString(const CxJsonValue *value) {
-    return value->value.string.ptr;
+    return value->string.ptr;
 }
 
 cxstring cxJsonAsCxString(const CxJsonValue *value) {
-    return cx_strcast(value->value.string);
+    return cx_strcast(value->string);
 }
 
 cxmutstr cxJsonAsCxMutStr(const CxJsonValue *value) {
-    return value->value.string;
+    return value->string;
 }
 
 double cxJsonAsDouble(const CxJsonValue *value) {
     if (value->type == CX_JSON_INTEGER) {
-        return (double) value->value.integer;
+        return (double) value->integer;
     } else {
-        return value->value.number;
+        return value->number;
     }
 }
 
 int64_t cxJsonAsInteger(const CxJsonValue *value) {
     if (value->type == CX_JSON_INTEGER) {
-        return value->value.integer;
+        return value->integer;
     } else {
-        return (int64_t) value->value.number;
+        return (int64_t) value->number;
     }
 }
 
 CxIterator cxJsonArrIter(const CxJsonValue *value) {
     return cxIteratorPtr(
-        value->value.array.array,
-        value->value.array.array_size,
+        value->array.data,
+        value->array.data_size,
         true // arrays need to keep order
     );
 }
 
-CxIterator cxJsonObjIter(const CxJsonValue *value) {
-    return cxIterator(
-        value->value.object.values,
-        sizeof(CxJsonObjValue),
-        value->value.object.values_size,
-        true // TODO: objects do not always need to keep order
-    );
+CxMapIterator cxJsonObjIter(const CxJsonValue *value) {
+    return cxMapIterator(value->object);
 }
 
 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) {
+    CxJsonValue *v = cxMapGet(value->object, name);
+    if (v == NULL) {
         return &cx_json_value_nothing;
     } else {
-        return value->value.object.values[index].value;
+        return v;
     }
 }
 
 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() / 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;
-    }
+    CxJsonValue *v = NULL;
+    cxMapRemoveAndGet(value->object, name, &v);
+    return v;
 }
 
 CxJsonWriter cxJsonWriterCompact(void) {
     return (CxJsonWriter) {
         false,
-        true,
         6,
         false,
         4,
@@ -1207,7 +1134,6 @@
 CxJsonWriter cxJsonWriterPretty(bool use_spaces) {
     return (CxJsonWriter) {
         true,
-        true,
         6,
         use_spaces,
         4,
@@ -1272,14 +1198,8 @@
                 expected++;
             }
             depth++;
-            size_t elem_count = value->value.object.values_size;
-            for (size_t look_idx = 0; look_idx < elem_count; look_idx++) {
-                // get the member either via index array or directly
-                size_t elem_idx = settings->sort_members
-                                      ? look_idx
-                                      : value->value.object.indices[look_idx];
-                CxJsonObjValue *member = &value->value.object.values[elem_idx];
-
+            CxMapIterator member_iter = cxJsonObjIter(value);
+            cx_foreach(const CxMapEntry *, member, member_iter) {
                 // possible indentation
                 if (settings->pretty) {
                     if (cx_json_writer_indent(target, wfunc, settings, depth)) {
@@ -1289,26 +1209,27 @@
 
                 // the name
                 actual += wfunc("\"", 1, 1, target);
-                cxmutstr name = escape_string(member->name, settings->escape_slash);
+                cxstring key = cx_strn(member->key->data, member->key->len);
+                cxmutstr name = escape_string(key, settings->escape_slash);
                 actual += wfunc(name.ptr, 1, name.length, target);
-                if (name.ptr != member->name.ptr) {
-                    cx_strfree(&name);
-                }
                 actual += wfunc("\"", 1, 1, target);
                 const char *obj_name_sep = ": ";
                 if (settings->pretty) {
                     actual += wfunc(obj_name_sep, 1, 2, target);
-                    expected += 4 + member->name.length;
+                    expected += 4 + name.length;
                 } else {
                     actual += wfunc(obj_name_sep, 1, 1, target);
-                    expected += 3 + member->name.length;
+                    expected += 3 + name.length;
+                }
+                if (name.ptr != key.ptr) {
+                    cx_strfree(&name);
                 }
 
                 // the value
                 if (cx_json_write_rec(target, member->value, wfunc, settings, depth)) return 1;
 
                 // end of object-value
-                if (look_idx < elem_count - 1) {
+                if (member_iter.index < member_iter.elem_count - 1) {
                     const char *obj_value_sep = ",\n";
                     if (settings->pretty) {
                         actual += wfunc(obj_value_sep, 1, 2, target);
@@ -1361,13 +1282,14 @@
         }
         case CX_JSON_STRING: {
             actual += wfunc("\"", 1, 1, target);
-            cxmutstr str = escape_string(value->value.string, settings->escape_slash);
+            cxmutstr str = escape_string(cx_strcast(value->string),
+                settings->escape_slash);
             actual += wfunc(str.ptr, 1, str.length, target);
-            if (str.ptr != value->value.string.ptr) {
+            actual += wfunc("\"", 1, 1, target);
+            expected += 2 + str.length;
+            if (str.ptr != value->string.ptr) {
                 cx_strfree(&str);
             }
-            actual += wfunc("\"", 1, 1, target);
-            expected += 2 + value->value.string.length;
             break;
         }
         case CX_JSON_NUMBER: {
@@ -1375,7 +1297,7 @@
             // because of the way how %g is defined, we need to
             // double the precision and truncate ourselves
             precision = 1 + (precision > 15 ? 30 : 2 * precision);
-            snprintf(numbuf, 40, "%.*g", precision, value->value.number);
+            snprintf(numbuf, 40, "%.*g", precision, value->number);
             char *dot, *exp;
             unsigned char max_digits;
             // find the decimal separator and hope that it's one of . or ,
@@ -1439,17 +1361,17 @@
             break;
         }
         case CX_JSON_INTEGER: {
-            snprintf(numbuf, 32, "%" PRIi64, value->value.integer);
+            snprintf(numbuf, 32, "%" PRIi64, value->integer);
             size_t len = strlen(numbuf);
             actual += wfunc(numbuf, 1, len, target);
             expected += len;
             break;
         }
         case CX_JSON_LITERAL: {
-            if (value->value.literal == CX_JSON_TRUE) {
+            if (value->literal == CX_JSON_TRUE) {
                 actual += wfunc("true", 1, 4, target);
                 expected += 4;
-            } else if (value->value.literal == CX_JSON_FALSE) {
+            } else if (value->literal == CX_JSON_FALSE) {
                 actual += wfunc("false", 1, 5, target);
                 expected += 5;
             } else {
--- a/ucx/kv_list.c	Thu Dec 11 22:58:02 2025 +0100
+++ b/ucx/kv_list.c	Fri Dec 12 10:42:53 2025 +0100
@@ -285,6 +285,7 @@
 
 static void cx_kvl_map_deallocate(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->map_methods->deallocate(map);
     kv_list->list_methods->deallocate(&kv_list->list.base);
 }
@@ -296,41 +297,7 @@
     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) {
+static 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
@@ -338,7 +305,7 @@
     return kv_list->list.base.collection.store_pointer ? *(void**)node_data : node_data;
 }
 
-int cx_kvl_map_remove(CxMap *map, CxHashKey key, void *targetbuf) {
+static 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;
@@ -381,6 +348,43 @@
     return 0;
 }
 
+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);
+    }
+
+    // remove any existing element first
+    cx_kvl_map_remove(map, key, NULL);
+
+    // now reserve new memory in the map
+    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;
+}
+
 static void *cx_kvl_iter_current_entry(const void *it) {
     const CxMapIterator *iter = it;
     return (void*)&iter->entry;
@@ -455,7 +459,7 @@
     return iter->elem != NULL;
 }
 
-CxMapIterator cx_kvl_map_iterator(const CxMap *map, enum cx_map_iterator_type type) {
+static CxMapIterator cx_kvl_map_iterator(const CxMap *map, enum cx_map_iterator_type type) {
     CxMapIterator iter = {0};
 
     iter.type = type;
--- a/ucx/properties.c	Thu Dec 11 22:58:02 2025 +0100
+++ b/ucx/properties.c	Fri Dec 12 10:42:53 2025 +0100
@@ -29,12 +29,15 @@
 #include "cx/properties.h"
 
 #include <assert.h>
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
 
 const CxPropertiesConfig cx_properties_config_default = {
-        '=',
-        '#',
-        '\0',
-        '\0',
+    '=',
+    '#',
+    '\0',
+    '\0',
     '\\',
 };
 
@@ -94,13 +97,30 @@
 
     // a pointer to the buffer we want to read from
     CxBuffer *current_buffer = &prop->input;
-
+    
+    char comment1 = prop->config.comment1;
+    char comment2 = prop->config.comment2;
+    char comment3 = prop->config.comment3;
+    char delimiter = prop->config.delimiter;
+    char continuation = prop->config.continuation;
+    
     // check if we have rescued data
     if (!cxBufferEof(&prop->buffer)) {
         // check if we can now get a complete line
         cxstring input = cx_strn(prop->input.space + prop->input.pos,
             prop->input.size - prop->input.pos);
         cxstring nl = cx_strchr(input, '\n');
+        while (nl.length > 0) {
+            // check for line continuation
+            char previous = nl.ptr > input.ptr ? nl.ptr[-1] : prop->buffer.space[prop->buffer.size-1];
+            if (previous == continuation) {
+                // this nl is a line continuation, check the next newline
+                nl = cx_strchr(cx_strsubs(nl, 1), '\n');
+            } else {
+                break;
+            }
+        }
+        
         if (nl.length > 0) {
             // we add as much data to the rescue buffer as we need
             // to complete the line
@@ -127,12 +147,7 @@
             return CX_PROPERTIES_INCOMPLETE_DATA;
         }
     }
-
-    char comment1 = prop->config.comment1;
-    char comment2 = prop->config.comment2;
-    char comment3 = prop->config.comment3;
-    char delimiter = prop->config.delimiter;
-
+   
     // get one line and parse it
     while (!cxBufferEof(current_buffer)) {
         const char *buf = current_buffer->space + current_buffer->pos;
@@ -145,6 +160,7 @@
         size_t delimiter_index = 0;
         size_t comment_index = 0;
         bool has_comment = false;
+        bool has_continuation = false;
 
         size_t i = 0;
         char c = 0;
@@ -159,6 +175,9 @@
                 if (delimiter_index == 0 && !has_comment) {
                     delimiter_index = i;
                 }
+            } else if (delimiter_index > 0 && c == continuation && i+1 < len && buf[i+1] == '\n') {
+                has_continuation = true;
+                i++;
             } else if (c == '\n') {
                 break;
             }
@@ -223,10 +242,53 @@
             k = cx_strtrim(k);
             val = cx_strtrim(val);
             if (k.length > 0) {
+                current_buffer->pos += i + 1; 
+                assert(current_buffer->pos <= current_buffer->size);
+                assert(current_buffer != &prop->buffer || current_buffer->pos == current_buffer->size);
+                
+                if (has_continuation) {
+                    char *ptr = (char*)val.ptr;
+                    if (current_buffer != &prop->buffer) {
+                        // move value to the rescue buffer
+                        if (prop->buffer.space == NULL) {
+                            cxBufferInit(&prop->buffer, NULL, 256, NULL, CX_BUFFER_AUTO_EXTEND);
+                        }
+                        prop->buffer.size = 0;
+                        prop->buffer.pos = 0;
+                        if (cxBufferWrite(val.ptr, 1, val.length, &prop->buffer) != val.length) {
+                            return CX_PROPERTIES_BUFFER_ALLOC_FAILED;
+                        }
+                        val.ptr = prop->buffer.space;
+                        ptr = prop->buffer.space;
+                    }
+                    // value.ptr is now inside the rescue buffer and we can
+                    // remove the continuation character from the value
+                    bool trim = false;
+                    size_t x = 0;
+                    for(size_t j=0;j<val.length;j++) {
+                        c = ptr[j];
+                        if (j+1 < val.length && c == '\\' && ptr[j+1] == '\n') {
+                            // skip continuation and newline character
+                            j++;
+                            trim = true; // enable trim in the next line
+                            continue;
+                        }
+                        if (j > x) {
+                            if (trim) {
+                                if (isspace((unsigned char)c)) {
+                                    continue;
+                                }
+                                trim = false;
+                            }
+                            ptr[x] = c;
+                        }
+                        x++;
+                    }
+                    val.length = x;
+                }
                 *key = k;
                 *value = val;
-                current_buffer->pos += i + 1;
-                assert(current_buffer->pos <= current_buffer->size);
+                
                 return CX_PROPERTIES_NO_ERROR;
             } else {
                 return CX_PROPERTIES_INVALID_EMPTY_KEY;
@@ -241,180 +303,96 @@
     return CX_PROPERTIES_NO_DATA;
 }
 
-static int cx_properties_sink_map(
-        cx_attr_unused CxProperties *prop,
-        CxPropertiesSink *sink,
-        cxstring key,
-        cxstring value
-) {
-    CxMap *map = sink->sink;
-    CxAllocator *alloc = sink->data;
-    cxmutstr v = cx_strdup_a(alloc, value);
-    int r = cxMapPut(map, key, v.ptr);
-    if (r != 0) cx_strfree_a(alloc, &v);
-    return r;
-}
-
-CxPropertiesSink cxPropertiesMapSink(CxMap *map) {
-    CxPropertiesSink sink;
-    sink.sink = map;
-    sink.data = (void*) cxDefaultAllocator;
-    sink.sink_func = cx_properties_sink_map;
-    return sink;
-}
+#ifndef CX_PROPERTIES_LOAD_FILL_SIZE
+#define CX_PROPERTIES_LOAD_FILL_SIZE 1024
+#endif
+const unsigned cx_properties_load_fill_size = CX_PROPERTIES_LOAD_FILL_SIZE;
+#ifndef CX_PROPERTIES_LOAD_BUF_SIZE
+#define CX_PROPERTIES_LOAD_BUF_SIZE 256
+#endif
+const unsigned cx_properties_load_buf_size = CX_PROPERTIES_LOAD_BUF_SIZE;
 
-static int cx_properties_read_string(
-        CxProperties *prop,
-        CxPropertiesSource *src,
-        cxstring *target
-) {
-    if (prop->input.space == src->src) {
-        // when the input buffer already contains the string
-        // we have nothing more to provide
-        target->length = 0;
-    } else {
-        target->ptr = src->src;
-        target->length = src->data_size;
+CxPropertiesStatus cx_properties_load(CxPropertiesConfig config,
+        const CxAllocator *allocator, cxstring filename, CxMap *target) {
+    if (allocator == NULL) {
+        allocator = cxDefaultAllocator;
     }
-    return 0;
-}
-
-static int cx_properties_read_file(
-        cx_attr_unused CxProperties *prop,
-        CxPropertiesSource *src,
-        cxstring *target
-) {
-    target->ptr = src->data_ptr;
-    target->length = fread(src->data_ptr, 1, src->data_size, src->src);
-    return ferror((FILE*)src->src);
-}
-
-static int cx_properties_read_init_file(
-        cx_attr_unused CxProperties *prop,
-        CxPropertiesSource *src
-) {
-    src->data_ptr = cxMallocDefault(src->data_size);
-    if (src->data_ptr == NULL) return 1;
-    return 0;
-}
 
-static void cx_properties_read_clean_file(
-        cx_attr_unused CxProperties *prop,
-        CxPropertiesSource *src
-) {
-    cxFreeDefault(src->data_ptr);
-}
-
-CxPropertiesSource cxPropertiesStringSource(cxstring str) {
-    CxPropertiesSource src;
-    src.src = (void*) str.ptr;
-    src.data_size = str.length;
-    src.data_ptr = NULL;
-    src.read_func = cx_properties_read_string;
-    src.read_init_func = NULL;
-    src.read_clean_func = NULL;
-    return src;
-}
-
-CxPropertiesSource cxPropertiesCstrnSource(const char *str, size_t len) {
-    CxPropertiesSource src;
-    src.src = (void*) str;
-    src.data_size = len;
-    src.data_ptr = NULL;
-    src.read_func = cx_properties_read_string;
-    src.read_init_func = NULL;
-    src.read_clean_func = NULL;
-    return src;
-}
+    // sanity check for the map
+    const bool use_cstring = cxCollectionStoresPointers(target);
+    if (!use_cstring && cxCollectionElementSize(target) != sizeof(cxmutstr)) {
+        return CX_PROPERTIES_MAP_ERROR;
+    }
 
-CxPropertiesSource cxPropertiesCstrSource(const char *str) {
-    CxPropertiesSource src;
-    src.src = (void*) str;
-    src.data_size = strlen(str);
-    src.data_ptr = NULL;
-    src.read_func = cx_properties_read_string;
-    src.read_init_func = NULL;
-    src.read_clean_func = NULL;
-    return src;
-}
+    // create a duplicate to guarantee zero-termination
+    cxmutstr fname = cx_strdup(filename);
+    if (fname.ptr == NULL) {
+        return CX_PROPERTIES_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE
+    }
 
-CxPropertiesSource cxPropertiesFileSource(FILE *file, size_t chunk_size) {
-    CxPropertiesSource src;
-    src.src = file;
-    src.data_size = chunk_size;
-    src.data_ptr = NULL;
-    src.read_func = cx_properties_read_file;
-    src.read_init_func = cx_properties_read_init_file;
-    src.read_clean_func = cx_properties_read_clean_file;
-    return src;
-}
-
-CxPropertiesStatus cxPropertiesLoad(
-        CxProperties *prop,
-        CxPropertiesSink sink,
-        CxPropertiesSource source
-) {
-    assert(source.read_func != NULL);
-    assert(sink.sink_func != NULL);
-
-    // initialize reader
-    if (source.read_init_func != NULL) {
-        if (source.read_init_func(prop, &source)) {
-            return CX_PROPERTIES_READ_INIT_FAILED;  // LCOV_EXCL_LINE
-        }
+    // open the file
+    FILE *f = fopen(fname.ptr, "r");
+    if (f == NULL) {
+        cx_strfree(&fname);
+        return CX_PROPERTIES_FILE_ERROR;
     }
 
-    // transfer the data from the source to the sink
+    // initialize the parser
+    char linebuf[cx_properties_load_buf_size];
+    char fillbuf[cx_properties_load_fill_size];
     CxPropertiesStatus status;
-    CxPropertiesStatus kv_status = CX_PROPERTIES_NO_DATA;
-    bool found = false;
-    while (true) {
-        // read input
-        cxstring input;
-        if (source.read_func(prop, &source, &input)) { // LCOV_EXCL_START
-            status = CX_PROPERTIES_READ_FAILED;
-            break;
-        } // LCOV_EXCL_STOP
+    CxProperties parser;
+    cxPropertiesInit(&parser, config);
+    cxPropertiesUseStack(&parser, linebuf, cx_properties_load_buf_size);
 
-        // no more data - break
-        if (input.length == 0) {
-            if (found) {
-                // something was found, check the last kv_status
-                if (kv_status == CX_PROPERTIES_INCOMPLETE_DATA) {
-                    status = CX_PROPERTIES_INCOMPLETE_DATA;
-                } else {
-                    status = CX_PROPERTIES_NO_ERROR;
-                }
-            } else {
-                // nothing found
-                status = CX_PROPERTIES_NO_DATA;
-            }
+    // read/fill/parse loop
+    status = CX_PROPERTIES_NO_DATA;
+    size_t keys_found = 0;
+    while (true) {
+        size_t r = fread(fillbuf, 1, cx_properties_load_fill_size, f);
+        if (ferror(f)) {
+            status = CX_PROPERTIES_FILE_ERROR;
+            break;
+        }
+        if (r == 0) {
             break;
         }
-
-        // set the input buffer and read the k/v-pairs
-        cxPropertiesFill(prop, input);
-
-        do {
-            cxstring key, value;
-            kv_status = cxPropertiesNext(prop, &key, &value);
-            if (kv_status == CX_PROPERTIES_NO_ERROR) {
-                found = true;
-                if (sink.sink_func(prop, &sink, key, value)) {
-                    kv_status = CX_PROPERTIES_SINK_FAILED;  // LCOV_EXCL_LINE
+        if (cxPropertiesFilln(&parser, fillbuf, r)) {
+            status = CX_PROPERTIES_BUFFER_ALLOC_FAILED;
+            break;
+        }
+        cxstring key, value;
+        while (true) {
+            status = cxPropertiesNext(&parser, &key, &value);
+            if (status != CX_PROPERTIES_NO_ERROR) {
+                break;
+            } else {
+                cxmutstr v = cx_strdup_a(allocator, value);
+                if (v.ptr == NULL) {
+                    status = CX_PROPERTIES_MAP_ERROR;
+                    break;
                 }
+                void *mv = use_cstring ? (void*)v.ptr : &v;
+                if (cxMapPut(target, key, mv)) {
+                    cx_strfree(&v);
+                    status = CX_PROPERTIES_MAP_ERROR;
+                    break;
+                }
+                keys_found++;
             }
-        } while (kv_status == CX_PROPERTIES_NO_ERROR);
-
-        if (kv_status > CX_PROPERTIES_OK) {
-            status = kv_status;
+        }
+        if (status > CX_PROPERTIES_OK) {
             break;
         }
     }
 
-    if (source.read_clean_func != NULL) {
-        source.read_clean_func(prop, &source);
+    // cleanup and exit
+    fclose(f);
+    cxPropertiesDestroy(&parser);
+    cx_strfree(&fname);
+    if (status == CX_PROPERTIES_NO_DATA && keys_found > 0) {
+        return CX_PROPERTIES_NO_ERROR;
+    } else {
+        return status;
     }
-
-    return status;
 }
--- a/ucx/tree.c	Thu Dec 11 22:58:02 2025 +0100
+++ b/ucx/tree.c	Fri Dec 12 10:42:53 2025 +0100
@@ -734,15 +734,15 @@
         if (node == NULL) return 1;  // LCOV_EXCL_LINE
         cx_tree_zero_pointers(node, cx_tree_node_layout(tree));
         tree->root = node;
-        tree->size = 1;
+        tree->collection.size = 1;
         return 0;
     }
     int result = cx_tree_add(data, tree->search, tree->node_create,
                 tree, &node, tree->root, cx_tree_node_layout(tree));
     if (0 == result) {
-        tree->size++;
+        tree->collection.size++;
     } else {
-        cxFree(tree->allocator, node);
+        cxFree(tree->collection.allocator, node);
     }
     return result;
 }
@@ -767,9 +767,9 @@
     void *failed;
     ins += cx_tree_add_iter(iter, n, tree->search, tree->node_create,
                                   tree, &failed, tree->root, cx_tree_node_layout(tree));
-    tree->size += ins;
+    tree->collection.size += ins;
     if (ins < n) {
-        cxFree(tree->allocator, failed);
+        cxFree(tree->collection.allocator, failed);
     }
     return ins;
 }
@@ -818,24 +818,21 @@
     assert(search_func != NULL);
     assert(search_data_func != NULL);
 
-    CxTree *tree = cxMalloc(allocator, sizeof(CxTree));
+    CxTree *tree = cxZalloc(allocator, sizeof(CxTree));
     if (tree == NULL) return NULL;  // LCOV_EXCL_LINE
 
     tree->cl = &cx_tree_default_class;
-    tree->allocator = allocator;
+    tree->collection.allocator = allocator;
     tree->node_create = create_func;
     tree->search = search_func;
     tree->search_data = search_data_func;
-    tree->simple_destructor = NULL;
-    tree->advanced_destructor = (cx_destructor_func2) cxFree;
-    tree->destructor_data = (void *) allocator;
+    tree->collection.advanced_destructor = (cx_destructor_func2) cxFree;
+    tree->collection.destructor_data = (void *) allocator;
     tree->loc_parent = loc_parent;
     tree->loc_children = loc_children;
     tree->loc_last_child = loc_last_child;
     tree->loc_prev = loc_prev;
     tree->loc_next = loc_next;
-    tree->root = NULL;
-    tree->size = 0;
 
     return tree;
 }
@@ -845,7 +842,7 @@
     if (tree->root != NULL) {
         cxTreeClear(tree);
     }
-    cxFree(tree->allocator, tree);
+    cxFree(tree->collection.allocator, tree);
 }
 
 CxTree *cxTreeCreateWrapped(const CxAllocator *allocator, void *root,
@@ -856,39 +853,33 @@
     }
     assert(root != NULL);
 
-    CxTree *tree = cxMalloc(allocator, sizeof(CxTree));
+    CxTree *tree = cxZalloc(allocator, sizeof(CxTree));
     if (tree == NULL) return NULL;  // LCOV_EXCL_LINE
 
     tree->cl = &cx_tree_default_class;
     // set the allocator anyway, just in case...
-    tree->allocator = allocator;
-    tree->node_create = NULL;
-    tree->search = NULL;
-    tree->search_data = NULL;
-    tree->simple_destructor = NULL;
-    tree->advanced_destructor = NULL;
-    tree->destructor_data = NULL;
+    tree->collection.allocator = allocator;
     tree->loc_parent = loc_parent;
     tree->loc_children = loc_children;
     tree->loc_last_child = loc_last_child;
     tree->loc_prev = loc_prev;
     tree->loc_next = loc_next;
     tree->root = root;
-    tree->size = cxTreeSubtreeSize(tree, root);
+    tree->collection.size = cxTreeSubtreeSize(tree, root);
     return tree;
 }
 
 void cxTreeSetParent(CxTree *tree, void *parent, void *child) {
     size_t loc_parent = tree->loc_parent;
     if (tree_parent(child) == NULL) {
-        tree->size++;
+        tree->collection.size++;
     }
     cx_tree_link(parent, child, cx_tree_node_layout(tree));
 }
 
 void cxTreeAddChildNode(CxTree *tree, void *parent, void *child) {
     cx_tree_link(parent, child, cx_tree_node_layout(tree));
-    tree->size++;
+    tree->collection.size++;
 }
 
 int cxTreeAddChild(CxTree *tree, void *parent, const void *data) {
@@ -896,7 +887,7 @@
     if (node == NULL) return 1; // LCOV_EXCL_LINE
     cx_tree_zero_pointers(node, cx_tree_node_layout(tree));
     cx_tree_link(parent, node, cx_tree_node_layout(tree));
-    tree->size++;
+    tree->collection.size++;
     return 0;
 }
 
@@ -948,7 +939,7 @@
 }
 
 size_t cxTreeSize(CxTree *tree) {
-    return tree->size;
+    return tree->collection.size;
 }
 
 size_t cxTreeDepth(CxTree *tree) {
@@ -1002,7 +993,7 @@
     if (loc_last_child >= 0) tree_last_child(node) = NULL;
 
     // the tree now has one member less
-    tree->size--;
+    tree->collection.size--;
 
     return 0;
 }
@@ -1010,12 +1001,12 @@
 void cxTreeRemoveSubtree(CxTree *tree, void *node) {
     if (node == tree->root) {
         tree->root = NULL;
-        tree->size = 0;
+        tree->collection.size = 0;
         return;
     }
     size_t subtree_size = cxTreeSubtreeSize(tree, node);
     cx_tree_unlink(node, cx_tree_node_layout(tree));
-    tree->size -= subtree_size;
+    tree->collection.size -= subtree_size;
 }
 
 int cxTreeDestroyNode(
@@ -1025,12 +1016,7 @@
 ) {
     int result = cxTreeRemoveNode(tree, node, relink_func);
     if (result == 0) {
-        if (tree->simple_destructor) {
-            tree->simple_destructor(node);
-        }
-        if (tree->advanced_destructor) {
-            tree->advanced_destructor(tree->destructor_data, node);
-        }
+        cx_invoke_destructor(tree, node);
         return 0;
     } else {
         return result;
@@ -1045,15 +1031,10 @@
     );
     cx_foreach(void *, child, iter) {
         if (iter.exiting) {
-            if (tree->simple_destructor) {
-                tree->simple_destructor(child);
-            }
-            if (tree->advanced_destructor) {
-                tree->advanced_destructor(tree->destructor_data, child);
-            }
+            cx_invoke_destructor(tree, child);
         }
     }
-    tree->size -= iter.counter;
+    tree->collection.size -= iter.counter;
     if (node == tree->root) {
         tree->root = NULL;
     }

mercurial