merge changes for Windows

Thu, 25 Dec 2025 11:48:25 +0100

author
Mike Becker <universe@uap-core.de>
date
Thu, 25 Dec 2025 11:48:25 +0100
changeset 1670
56f55f2f70c0
parent 1669
d416628d6c7d (diff)
parent 1666
1ac3c150ec56 (current diff)
child 1671
cf19b7820ff0

merge changes for Windows

src/properties.c file | annotate | diff | comparison | revisions
--- a/CHANGELOG	Wed Dec 24 15:05:36 2025 +0100
+++ b/CHANGELOG	Thu Dec 25 11:48:25 2025 +0100
@@ -11,6 +11,7 @@
  * adds line continuation support to CxProperties / CxPropertiesConfig
  * adds cx_hash_key_as_string()
  * adds support for CxHashKey pointers in CX_HASH_KEY() and all map functions
+ * adds cx_strat()
  * adds cx_bstr() and cx_bstr_m()
  * adds cxBufferMaximumCapacity()
  * adds cxBufferAppendString()
@@ -21,6 +22,7 @@
  * changes cxBufferTerminate() so that position and size are equal after a successful operation
  * changes cxBufferPutString() to accept any kind of string that cx_strcast() supports
  * changes that multiple string.h functions now also automatically apply cx_strcast() where possible
+ * changes cx_strcat() family of function to return an int and removes two unnecessary variants
  * changes the names of cxDefineDestructor() and cxDefineAdvancedDestructor() to
    cxSetDestructor() and cxSetdvancedDestructor()
  * changes the name of cxCollectionCompareFunc() to cxSetCompareFunc()
--- a/docs/Writerside/topics/about.md	Wed Dec 24 15:05:36 2025 +0100
+++ b/docs/Writerside/topics/about.md	Thu Dec 25 11:48:25 2025 +0100
@@ -38,6 +38,7 @@
 * adds line continuation support to CxProperties / CxPropertiesConfig
 * adds cx_hash_key_as_string()
 * adds support for CxHashKey pointers in CX_HASH_KEY() and all map functions
+* adds cx_strat()
 * adds cx_bstr() and cx_bstr_m()
 * adds cxBufferMaximumCapacity()
 * adds cxBufferAppendString()
@@ -48,6 +49,7 @@
 * changes cxBufferTerminate() so that position and size are equal after a successful operation
 * changes cxBufferPutString() to accept any kind of string that cx_strcast() supports
 * changes that multiple string.h functions now also automatically apply cx_strcast() where possible
+* changes cx_strcat() family of function to return an int and removes two unnecessary variants
 * changes the names of cxDefineDestructor() and cxDefineAdvancedDestructor() to
   cxSetDestructor() and cxSetdvancedDestructor()
 * changes the name of cxCollectionCompareFunc() to cxSetCompareFunc()
--- a/docs/Writerside/topics/string.h.md	Wed Dec 24 15:05:36 2025 +0100
+++ b/docs/Writerside/topics/string.h.md	Thu Dec 25 11:48:25 2025 +0100
@@ -119,27 +119,24 @@
 ```C
 #include <cx/string.h>
 
-cxmutstr cx_strcat(size_t count, ... );
-
-cxmutstr cx_strcat_a(const CxAllocator *alloc, size_t count, ... );
+int cx_strcat(cxmutstr *str, size_t count, ... );
 
-cxmutstr cx_strcat_m(cxmutstr str, size_t count, ... );
-
-cxmutstr cx_strcat_ma(const CxAllocator *alloc,
-        cxmutstr str, size_t count, ... );
+int cx_strcat_a(const CxAllocator *allocator,
+        cxmutstr *str, size_t count, ... );
 
 size_t cx_strlen(size_t count, ...);
 ```
 
-The `cx_strcat_a()` function takes `count` UCX strings (`cxstring` or `cxmutstr`),
-allocates memory for a concatenation of those strings _with a single allocation_,
-and copies the contents of the strings to the new memory.
+The `cx_strcat_a()` function takes `count` UCX strings (`cxstring` or `cxmutstr` - not pointers!),
+allocates memory in `str` for a concatenation of those strings _with a single allocation_,
+and appends the contents of the strings to `str`.
 `cx_strcat()` is equivalent, except that it uses the [default allocator](allocator.h.md#default-allocator).
 
-The `cx_strcat_ma()` and `cx_strcat_m()` append the `count` strings to the specified string `str` and,
-instead of allocating new memory, reallocate the existing memory in `str`.
-If the pointer in `str` is `NULL`, there is no difference to `cx_strcat_a()`.
-Note, that `count` always denotes the number of variadic arguments in _both_ variants.
+Example usage:
+```C
+cxmutstr str = {0};
+cx_strcat(&str, 2, cx_str("Hello, "), cx_str("World!"));
+```
 
 The function `cx_strlen()` sums the length of the specified strings.
 
@@ -153,6 +150,8 @@
 ```C
 #include <cx/string.h>
 
+char cx_strat(cxstring str, off_t index);
+
 cxstring cx_strchr(cxstring string, int chr);
 
 cxstring cx_strrchr(cxstring string, int chr);
@@ -178,6 +177,10 @@
 cxmutstr cx_strtrim_m(cxmutstr string);
 ```
 
+The function `cx_strat()` returns the character at the specified `index`.
+When the `index` is negative, the characters are counted from the end of the string.
+When the `index` is out of bounds, the zero-character is returned.
+
 The functions `cx_strchr()`, `cx_strrchr()`, and `cx_strstr()`, behave like their stdlib counterparts.
 
 The function `cx_strsubs()` returns the substring starting at the specified `start` index,
--- a/src/cx/json.h	Wed Dec 24 15:05:36 2025 +0100
+++ b/src/cx/json.h	Thu Dec 25 11:48:25 2025 +0100
@@ -289,13 +289,6 @@
     CxBuffer buffer;
 
     /**
-     * Used internally.
-     *
-     * Remembers the prefix of the last uncompleted token.
-     */
-    CxJsonToken uncompleted;
-
-    /**
      * A pointer to an intermediate state of the currently parsed value.
      *
      * Never access this value manually.
@@ -310,6 +303,16 @@
     cxmutstr uncompleted_member_name;
 
     /**
+     * Internal buffer for uncompleted tokens.
+     */
+    cxmutstr uncompleted_content;
+
+    /**
+     * The expected type of the currently parsed, uncompleted token.
+     */
+    CxJsonTokenType uncompleted_tokentype;
+
+    /**
      * State stack.
      */
     CX_ARRAY(int, states);
--- a/src/cx/string.h	Wed Dec 24 15:05:36 2025 +0100
+++ b/src/cx/string.h	Thu Dec 25 11:48:25 2025 +0100
@@ -416,9 +416,6 @@
  * If @p str already contains a string, the memory will be reallocated and
  * the other strings are appended. Otherwise, new memory is allocated.
  *
- * If memory allocation fails, the pointer in the returned string will
- * be @c NULL. Depending on the allocator, @c errno might be set.
- *
  * @note It is guaranteed that there is only one allocation for the
  * resulting string.
  * It is also guaranteed that the returned string is zero-terminated.
@@ -427,32 +424,12 @@
  * @param str   the string the other strings shall be concatenated to
  * @param count the number of the other following strings to concatenate
  * @param ...   all other UCX strings
- * @return the concatenated string
+ * @retval zero success
+ * @retval non-zero allocation failure
  */
-cx_attr_nodiscard cx_attr_nonnull
-CX_EXPORT cxmutstr cx_strcat_ma(const CxAllocator *alloc,
-        cxmutstr str, size_t count, ...);
-
-/**
- * Concatenates strings and returns a new string.
- *
- * The resulting string will be allocated by the specified allocator.
- * So developers @em must pass the return value to cx_strfree_a() eventually.
- *
-* If memory allocation fails, the pointer in the returned string will
- * be @c NULL. Depending on the allocator, @c errno might be set.
- *
- * @note It is guaranteed that there is only one allocation for the
- * resulting string.
- * It is also guaranteed that the returned string is zero-terminated.
- *
- * @param alloc (@c CxAllocator*) the allocator to use
- * @param count (@c size_t) the number of the other following strings to concatenate
- * @param ...   all other UCX strings
- * @return (@c cxmutstr) the concatenated string
- */
-#define cx_strcat_a(alloc, count, ...) \
-        cx_strcat_ma(alloc, cx_mutstrn(NULL, 0), count, __VA_ARGS__)
+cx_attr_nonnull
+CX_EXPORT int cx_strcat_a(const CxAllocator *alloc,
+        cxmutstr *str, size_t count, ...);
 
 /**
  * Concatenates strings and returns a new string.
@@ -460,43 +437,18 @@
  * The resulting string will be allocated by the cxDefaultAllocator.
  * So developers @em must pass the return value to cx_strfree() eventually.
  *
-* If memory allocation fails, the pointer in the returned string will
- * be @c NULL and @c errno might be set.
- *
  * @note It is guaranteed that there is only one allocation for the
  * resulting string.
  * It is also guaranteed that the returned string is zero-terminated.
  *
+ * @param str (@c cxmutstr*) the string the other strings shall be concatenated to
  * @param count (@c size_t) the number of the other following strings to concatenate
  * @param ... all other UCX strings
- * @return (@c cxmutstr) the concatenated string
+ * @retval zero success
+ * @retval non-zero allocation failure
  */
-#define cx_strcat(count, ...) \
-        cx_strcat_ma(cxDefaultAllocator, cx_mutstrn(NULL, 0), count, __VA_ARGS__)
-
-/**
- * Concatenates strings.
- *
- * The resulting string will be allocated by the cxDefaultAllocator.
- * So developers @em must pass the return value to cx_strfree() eventually.
- *
- * If @p str already contains a string, the memory will be reallocated and
- * the other strings are appended. Otherwise, new memory is allocated.
- *
-* If memory allocation fails, the pointer in the returned string will
- * be @c NULL and @c errno might be set.
- *
- * @note It is guaranteed that there is only one allocation for the
- * resulting string.
- * It is also guaranteed that the returned string is zero-terminated.
- *
- * @param str (@c cxmutstr) the string the other strings shall be concatenated to
- * @param count (@c size_t) the number of the other following strings to concatenate
- * @param ... all other strings
- * @return (@c cxmutstr) the concatenated string
- */
-#define cx_strcat_m(str, count, ...) \
-        cx_strcat_ma(cxDefaultAllocator, str, count, __VA_ARGS__)
+#define cx_strcat(str, count, ...) \
+        cx_strcat_a(cxDefaultAllocator, str, count, __VA_ARGS__)
 
 /**
  * Returns a substring starting at the specified location.
@@ -579,6 +531,44 @@
 CX_EXPORT cxmutstr cx_strsubsl_m(cxmutstr string, size_t start, size_t length);
 
 /**
+ * Returns the character at the specified index offset.
+ *
+ * Internal function - do not use.
+ *
+ * @param str the string
+ * @param index the index offset
+ * @return the character at the index
+ * @see cx_strat()
+ */
+CX_INLINE char cx_strat_(cxstring str, off_t index) {
+    size_t i;
+    if (index >= 0) {
+        i = index;
+    } else {
+        i = (size_t) (str.length + index);
+    }
+    if (i >= str.length) {
+        return '\0';
+    }
+    return str.ptr[i];
+}
+
+/**
+ * Returns the character at the specified index offset.
+ *
+ * When the @p index is negative, the character is counted from the end of the
+ * string where -1 denotes the last character in the string.
+ *
+ * When the @p index is out of bounds, the function returns zero.
+ *
+ * @param str the string
+ * @param index the index offset
+ * @return the character at the index
+ * @see cx_strat()
+ */
+#define cx_strat(str, index) cx_strat_(cx_strcast(str), index)
+
+/**
  * Returns a substring starting at the location of the first occurrence of the
  * specified character.
  *
--- a/src/json.c	Wed Dec 24 15:05:36 2025 +0100
+++ b/src/json.c	Thu Dec 25 11:48:25 2025 +0100
@@ -96,23 +96,27 @@
 }
 
 static CxJsonToken token_create(CxJson *json, bool isstring, size_t start, size_t end) {
-    cxmutstr str = cx_mutstrn(json->buffer.space + start, end - start);
-    bool allocated = false;
-    if (json->uncompleted.tokentype != CX_JSON_NO_TOKEN) {
+    cxmutstr buf_str = cx_mutstrn(json->buffer.space + start, end - start);
+    cxmutstr str;
+    bool allocated;
+    if (json->uncompleted_tokentype != CX_JSON_NO_TOKEN) {
         allocated = true;
-        str = cx_strcat_m(json->uncompleted.content, 1, str);
-        if (str.ptr == NULL) { // LCOV_EXCL_START
-            return (CxJsonToken){CX_JSON_NO_TOKEN, false, {NULL, 0}};
-        } // LCOV_EXCL_STOP
+        str = json->uncompleted_content;
+        if (cx_strcat(&str, 1, buf_str)) {
+            return (CxJsonToken){CX_JSON_NO_TOKEN, false, {NULL, 0}}; // LCOV_EXCL_LINE
+        }
+        json->uncompleted_content = (cxmutstr){NULL, 0};
+        json->uncompleted_tokentype = CX_JSON_NO_TOKEN;
+    } else {
+        allocated = false;
+        str = buf_str;
     }
-    json->uncompleted = (CxJsonToken){0};
     CxJsonTokenType ttype;
     if (isstring) {
         ttype = CX_JSON_TOKEN_STRING;
     } else {
-        cxstring s = cx_strcast(str);
-        if (!cx_strcmp(s, "true") || !cx_strcmp(s, "false")
-            || !cx_strcmp(s, "null")) {
+        if (!cx_strcmp(str, "true") || !cx_strcmp(str, "false")
+            || !cx_strcmp(str, "null")) {
             ttype = CX_JSON_TOKEN_LITERAL;
         } else {
             ttype = token_numbertype(str.ptr, str.length);
@@ -162,16 +166,16 @@
 static enum cx_json_status token_parse_next(CxJson *json, CxJsonToken *result) {
     // check if there is data in the buffer
     if (cxBufferEof(&json->buffer)) {
-        return json->uncompleted.tokentype == CX_JSON_NO_TOKEN ?
+        return json->uncompleted_tokentype == CX_JSON_NO_TOKEN ?
             CX_JSON_NO_DATA : CX_JSON_INCOMPLETE_DATA;
     }
 
     // current token type and start index
-    CxJsonTokenType ttype = json->uncompleted.tokentype;
+    CxJsonTokenType ttype = json->uncompleted_tokentype;
     size_t token_part_start = json->buffer.pos;
 
     bool escape_end_of_string = ttype == CX_JSON_TOKEN_STRING
-        && json->uncompleted.content.ptr[json->uncompleted.content.length-1] == '\\';
+        && cx_strat(json->uncompleted_content, -1) == '\\';
 
     for (size_t i = json->buffer.pos; i < json->buffer.size; i++) {
         char c = json->buffer.space[i];
@@ -232,31 +236,23 @@
         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) {
-            // current token is uncompleted
-            // save current token content
-            CxJsonToken uncompleted = {
-                ttype, true,
-                cx_strdup(cx_strn(json->buffer.space + token_part_start, uncompleted_len))
-            };
-            if (uncompleted.content.ptr == NULL) {
+        cxstring uncompleted = cx_strn(json->buffer.space + token_part_start, json->buffer.size - token_part_start);
+        if (json->uncompleted_tokentype == CX_JSON_NO_TOKEN) {
+            assert(json->uncompleted_content.ptr == NULL);
+            json->uncompleted_content = cx_strdup(uncompleted);
+            if (json->uncompleted_content.ptr == NULL) {
                 return CX_JSON_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE
             }
-            json->uncompleted = uncompleted;
+            json->uncompleted_tokentype = ttype;
         } else {
             // previously we also had an uncompleted token
             // combine the uncompleted token with the current token
-            assert(json->uncompleted.allocated);
-            cxmutstr str = cx_strcat_m(json->uncompleted.content, 1,
-                cx_strn(json->buffer.space + token_part_start, uncompleted_len));
-            if (str.ptr == NULL) {
+            if (cx_strcat(&json->uncompleted_content, 1, uncompleted)) {
                 return CX_JSON_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE
             }
-            json->uncompleted.content = str;
         }
         // advance the buffer position - we saved the stuff in the uncompleted token
-        json->buffer.pos += uncompleted_len;
+        json->buffer.pos += uncompleted.length;
         return CX_JSON_INCOMPLETE_DATA;
     }
 }
@@ -564,7 +560,8 @@
     }
     cxJsonValueFree(json->parsed);
     json->parsed = NULL;
-    token_destroy(&json->uncompleted);
+    json->uncompleted_tokentype = CX_JSON_NO_TOKEN;
+    cx_strfree(&json->uncompleted_content);
     cx_strfree_a(json->allocator, &json->uncompleted_member_name);
 }
 
--- a/src/properties.c	Wed Dec 24 15:05:36 2025 +0100
+++ b/src/properties.c	Thu Dec 25 11:48:25 2025 +0100
@@ -112,7 +112,7 @@
         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];
+            char previous = nl.ptr > input.ptr ? nl.ptr[-1] : cx_strat(cx_bstr(&prop->buffer), -1);
             if (previous == continuation) {
                 // this nl is a line continuation, check the next newline
                 nl = cx_strchr(cx_strsubs(nl, 1), '\n');
--- a/src/string.c	Wed Dec 24 15:05:36 2025 +0100
+++ b/src/string.c	Thu Dec 25 11:48:25 2025 +0100
@@ -99,13 +99,13 @@
     return size;
 }
 
-cxmutstr cx_strcat_ma(
+int cx_strcat_a(
         const CxAllocator *alloc,
-        cxmutstr str,
+        cxmutstr *str,
         size_t count,
         ...
 ) {
-    if (count == 0) return str;
+    if (count == 0) return 0;
     va_list ap;
     va_start(ap, count);
     va_list ap2;
@@ -113,7 +113,7 @@
 
     // compute overall length
     bool overflow = false;
-    size_t slen = str.length;
+    size_t slen = str->length;
     for (size_t i = 0; i < count; i++) {
         cxstring s = va_arg(ap, cxstring);
         if (slen > SIZE_MAX - s.length) overflow = true;
@@ -125,36 +125,31 @@
     if (overflow) {
         va_end(ap2);
         errno = EOVERFLOW;
-        return (cxmutstr) { NULL, 0 };
+        return -1;
     }
 
     // reallocate or create new string
-    char *newstr;
-    if (str.ptr == NULL) {
-        newstr = cxMalloc(alloc, slen + 1);
-    } else {
-        newstr = cxRealloc(alloc, str.ptr, slen + 1);
+    if (cxReallocate(alloc, &str->ptr, slen + 1)) {
+        // LCOV_EXCL_START
+        va_end(ap2);
+        return -1;
+        // LCOV_EXCL_STOP
     }
-    if (newstr == NULL) { // LCOV_EXCL_START
-        va_end(ap2);
-        return (cxmutstr) {NULL, 0};
-    } // LCOV_EXCL_STOP
-    str.ptr = newstr;
 
     // concatenate strings
-    size_t pos = str.length;
-    str.length = slen;
+    size_t pos = str->length;
+    str->length = slen;
     for (size_t i = 0; i < count; i++) {
         cxstring s = va_arg(ap2, cxstring);
-        memcpy(str.ptr + pos, s.ptr, s.length);
+        memcpy(str->ptr + pos, s.ptr, s.length);
         pos += s.length;
     }
     va_end(ap2);
 
     // terminate string
-    str.ptr[str.length] = '\0';
+    str->ptr[str->length] = '\0';
 
-    return str;
+    return 0;
 }
 
 cxstring cx_strsubs(
@@ -510,11 +505,11 @@
 
 cxstring cx_strtrim(cxstring string) {
     cxstring result = string;
-    while (result.length > 0 && isspace((unsigned char)(result.ptr[0]))) {
+    while (isspace((unsigned char)cx_strat(result, 0))) {
         result.ptr++;
         result.length--;
     }
-    while (result.length > 0 && isspace((unsigned char)result.ptr[result.length - 1])) {
+    while (isspace((unsigned char)cx_strat(result, -1))) {
         result.length--;
     }
     return result;
--- a/tests/test_properties.c	Wed Dec 24 15:05:36 2025 +0100
+++ b/tests/test_properties.c	Thu Dec 25 11:48:25 2025 +0100
@@ -384,6 +384,32 @@
     free(long_value);
 }
 
+CX_TEST(test_properties_next_starts_with_newlines) {
+    const char *str1 = "key1 = value1";
+    const char *str2 = "\nkey2 = value2\n";
+
+    CxProperties prop;
+    cxPropertiesInitDefault(&prop);
+
+    cxstring key;
+    cxstring value;
+
+    CX_TEST_DO {
+        CX_TEST_ASSERT(0 == cxPropertiesFill(&prop, str1));
+        CX_TEST_ASSERT(cxPropertiesNext(&prop, &key,  &value) == CX_PROPERTIES_INCOMPLETE_DATA);
+        CX_TEST_ASSERT(0 == cxPropertiesFill(&prop, str2));
+        CX_TEST_ASSERT(cxPropertiesNext(&prop, &key,  &value) == CX_PROPERTIES_NO_ERROR);
+        CX_TEST_ASSERT(!cx_strcmp(key, "key1"));
+        CX_TEST_ASSERT(!cx_strcmp(value, "value1"));
+        CX_TEST_ASSERT(cxPropertiesNext(&prop, &key,  &value) == CX_PROPERTIES_NO_ERROR);
+        CX_TEST_ASSERT(!cx_strcmp(key, "key2"));
+        CX_TEST_ASSERT(!cx_strcmp(value, "value2"));
+        CX_TEST_ASSERT(cxPropertiesNext(&prop, &key,  &value) == CX_PROPERTIES_NO_DATA);
+    }
+
+    cxPropertiesDestroy(&prop);
+}
+
 CX_TEST(test_properties_next_line_continuation) {
     const char *str = 
         "key1 = multiline \\\nvalue\n"
@@ -836,6 +862,7 @@
     cx_test_register(suite, test_properties_next_multi);
     cx_test_register(suite, test_properties_next_part);
     cx_test_register(suite, test_properties_next_long_lines);
+    cx_test_register(suite, test_properties_next_starts_with_newlines);
     cx_test_register(suite, test_properties_next_line_continuation);
     cx_test_register(suite, test_properties_next_line_continuation_part);
     cx_test_register(suite, test_properties_load);
--- a/tests/test_string.c	Wed Dec 24 15:05:36 2025 +0100
+++ b/tests/test_string.c	Thu Dec 25 11:48:25 2025 +0100
@@ -220,6 +220,31 @@
     }
 }
 
+CX_TEST(test_strat) {
+    cxstring str = cx_str("Hello, World!");
+
+    CX_TEST_DO {
+        // the entire string
+        for (size_t i = 0; i < str.length; i++) {
+            CX_TEST_ASSERT(cx_strat(str, i) == str.ptr[i]);
+        }
+        // the entire string backwards
+        for (off_t i = 1; i <= (off_t) str.length; i++) {
+            CX_TEST_ASSERT(cx_strat(str, -i) == str.ptr[str.length-i]);
+        }
+        // out of bounds (positive)
+        CX_TEST_ASSERT(cx_strat(str, 12) == '!');
+        CX_TEST_ASSERT(cx_strat(str, 13) == '\0');
+        CX_TEST_ASSERT(cx_strat(str, 14) == '\0');
+        CX_TEST_ASSERT(cx_strat(str, 651273) == '\0');
+        // out of bounds (negative)
+        CX_TEST_ASSERT(cx_strat(str, -13) == 'H');
+        CX_TEST_ASSERT(cx_strat(str, -14) == '\0');
+        CX_TEST_ASSERT(cx_strat(str, -15) == '\0');
+        CX_TEST_ASSERT(cx_strat(str, -651273) == '\0');
+    }
+}
+
 CX_TEST(test_strchr) {
     cxstring str = cx_str("I will find you - and I will kill you");
 
@@ -376,22 +401,26 @@
     CxAllocator *alloc = &talloc.base;
 
     CX_TEST_DO {
-        cxmutstr t1 = cx_strcat_a(alloc, 2, s1, s2);
+        cxmutstr t1 = {0};
+        CX_TEST_ASSERT(0 == cx_strcat_a(alloc, &t1, 2, s1, s2));
         CX_TEST_ASSERT(0 == cx_strcmp(t1, "1234"));
         ASSERT_ZERO_TERMINATED(t1);
         cx_strfree_a(alloc, &t1);
 
-        cxmutstr t2 = cx_strcat_a(alloc, 3, s1, s2, s3);
+        cxmutstr t2 = {0};
+        CX_TEST_ASSERT(0 == cx_strcat_a(alloc, &t2, 3, s1, s2, s3));
         CX_TEST_ASSERT(0 == cx_strcmp(t2, "123456"));
         ASSERT_ZERO_TERMINATED(t2);
         cx_strfree_a(alloc, &t2);
 
-        cxmutstr t3 = cx_strcat_a(alloc, 6, s1, sn, s2, sn, s3, sn);
+        cxmutstr t3 = {0};
+        CX_TEST_ASSERT(0 == cx_strcat_a(alloc, &t3, 6, s1, sn, s2, sn, s3, sn));
         CX_TEST_ASSERT(0 == cx_strcmp(t3, "123456"));
         ASSERT_ZERO_TERMINATED(t3);
         cx_strfree_a(alloc, &t3);
 
-        cxmutstr t4 = cx_strcat_a(alloc, 2, sn, sn);
+        cxmutstr t4 = {0};
+        CX_TEST_ASSERT(0 == cx_strcat_a(alloc, &t4, 2, sn, sn));
         CX_TEST_ASSERT(0 == cx_strcmp(t4, ""));
         ASSERT_ZERO_TERMINATED(t4);
         cx_strfree_a(alloc, &t4);
@@ -399,14 +428,15 @@
         CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc));
 
         // use the macro
-        cxmutstr t5 = cx_strcat(3, s3, s1, s2);
+        cxmutstr t5 = {0};
+        CX_TEST_ASSERT(0 == cx_strcat(&t5, 3, s3, s1, s2));
         CX_TEST_ASSERT(0 == cx_strcmp(t5, "561234"));
         ASSERT_ZERO_TERMINATED(t5);
         cx_strfree(&t5);
 
         // use an initial string
         cxmutstr t6 = cx_strdup(cx_str("Hello"));
-        t6 = cx_strcat_m(t6, 2, cx_str(", "), cx_str("World!"));
+        CX_TEST_ASSERT(0 == cx_strcat(&t6, 2, cx_str(", "), cx_str("World!")));
         CX_TEST_ASSERT(0 == cx_strcmp(t6, "Hello, World!"));
         ASSERT_ZERO_TERMINATED(t6);
         cx_strfree(&t6);
@@ -417,7 +447,8 @@
         cxstring b = cx_strn(fakestr, SIZE_MAX / 3 - 5);
         cxstring c = cx_strn(fakestr, SIZE_MAX / 3 + 20);
         errno = 0;
-        cxmutstr z = cx_strcat(3, a, b, c);
+        cxmutstr z = {0};
+        CX_TEST_ASSERT(0 != cx_strcat(&z, 3, a, b, c));
         CX_TEST_ASSERT(errno == EOVERFLOW);
         CX_TEST_ASSERT(z.ptr == NULL);
         CX_TEST_ASSERT(z.length == 0);
@@ -437,7 +468,8 @@
     cxstring s9 = cx_str("xy");
 
     CX_TEST_DO {
-        cxmutstr r = cx_strcat(9, s1, s2, s3, s4, s5, s6, s7, s8, s9);
+        cxmutstr r = {0};
+        CX_TEST_ASSERT(0 == cx_strcat(&r, 9, s1, s2, s3, s4, s5, s6, s7, s8, s9));
         CX_TEST_ASSERT(0 == cx_strcmp(r, "123456789abcdef0xy"));
         ASSERT_ZERO_TERMINATED(r);
         cx_strfree(&r);
@@ -1504,6 +1536,7 @@
     cx_test_register(suite, test_strcpy);
     cx_test_register(suite, test_strlen);
     cx_test_register(suite, test_strsubs);
+    cx_test_register(suite, test_strat);
     cx_test_register(suite, test_strchr);
     cx_test_register(suite, test_strrchr);
     cx_test_register(suite, test_strstr);

mercurial