ucx/json.c

branch
dav-2
changeset 891
4d58cbcc9efa
parent 889
42cdbf9bbd49
--- a/ucx/json.c	Sun Dec 07 20:16:59 2025 +0100
+++ b/ucx/json.c	Fri Dec 19 17:53:18 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;
-        }
-    }
-
-    // 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;
     }
 }
 
@@ -190,8 +111,8 @@
         ttype = CX_JSON_TOKEN_STRING;
     } else {
         cxstring s = cx_strcast(str);
-        if (!cx_strcmp(s, CX_STR("true")) || !cx_strcmp(s, CX_STR("false"))
-            || !cx_strcmp(s, CX_STR("null"))) {
+        if (!cx_strcmp(s, "true") || !cx_strcmp(s, "false")
+            || !cx_strcmp(s, "null")) {
             ttype = CX_JSON_TOKEN_LITERAL;
         } else {
             ttype = token_numbertype(str.ptr, str.length);
@@ -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
@@ -437,7 +359,7 @@
             } else if (c == 'u') {
                 char utf8buf[4];
                 unsigned utf8len = unescape_unicode_string(
-                    cx_strn(str.ptr + i - 1, str.length + 1 - i),
+                    cx_strn(str.ptr + i - 1, str.length - i),
                     utf8buf
                 );
                 if(utf8len > 0) {
@@ -456,7 +378,7 @@
             } else {
                 // TODO: discuss the behavior for unrecognized escape sequences
                 //       most parsers throw an error here - we just ignore it
-                result.ptr[result.length++] = '\\';
+                result.ptr[result.length++] = '\\'; // LCOV_EXCL_LINE
             }
 
             result.ptr[result.length++] = c;
@@ -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};
@@ -488,7 +410,7 @@
             size_t capa = str.length + 32;
             char *space = cxMallocDefault(capa);
             if (space == NULL) return cx_mutstrn(NULL, 0);
-            cxBufferInit(&buf, space, capa, NULL, CX_BUFFER_AUTO_EXTEND);
+            cxBufferInit(&buf, NULL, space, capa, CX_BUFFER_AUTO_EXTEND);
             cxBufferWrite(str.ptr, 1, i, &buf);
             all_printable = false;
         }
@@ -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) {
+    CxMap *map = cxKvListCreateAsMap(allocator, CX_STORE_POINTERS);
+    if (map == NULL) return NULL; // LCOV_EXCL_LINE
+    cxSetCompareFunc(map, cxJsonCompare);
+    cxSetDestructor(map, cxJsonValueFree);
+    return map;
+}
+
+static void json_free_object_map(CxJsonObject obj) {
+    cxMapFree(obj);
 }
 
 static CxJsonValue* json_create_value(CxJson *json, CxJsonValueType type) {
@@ -534,33 +472,30 @@
     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
+        if (cx_array_init_a(json->allocator, v->array, 16)) {
+            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
-    if (json->vbuf_size > 0) {
-        CxJsonValue *parent = json->vbuf[json->vbuf_size - 1];
+    if (json->vbuf.size > 0) {
+        CxJsonValue *parent = json->vbuf.data[json->vbuf.size - 1];
         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_add_a(json->allocator, parent->array, 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
         }
@@ -568,10 +503,19 @@
 
     // add the new value to the stack, if it is an array or object
     if (type == CX_JSON_ARRAY || type == CX_JSON_OBJECT) {
-        CxArrayReallocator vbuf_realloc = cx_array_reallocator(NULL, json->vbuf_internal);
-        if (cx_array_simple_add_a(&vbuf_realloc, json->vbuf, v)) {
-            goto create_json_value_exit_error; // LCOV_EXCL_LINE
+        if (json->vbuf.size >= json->vbuf.capacity) {
+            int alloc_error;
+            if (json->vbuf.data == json->vbuf_internal) {
+                alloc_error = cx_array_copy_to_new(json->vbuf, json->vbuf.size+1);
+            } else {
+                alloc_error = cx_array_reserve(json->vbuf, json->vbuf.size+1);
+            }
+            if (alloc_error) {
+                goto create_json_value_exit_error; // LCOV_EXCL_LINE
+            }
         }
+        json->vbuf.data[json->vbuf.size] = v;
+        json->vbuf.size++;
     }
 
     // if currently no value is parsed, this is now the value of interest
@@ -605,29 +549,23 @@
     memset(json, 0, sizeof(CxJson));
     json->allocator = allocator;
 
-    json->states = json->states_internal;
-    json->states_capacity = cx_nmemb(json->states_internal);
-    json->states[0] = JP_STATE_VALUE_BEGIN;
-    json->states_size = 1;
-
-    json->vbuf = json->vbuf_internal;
-    json->vbuf_capacity = cx_nmemb(json->vbuf_internal);
+    cx_array_init_fixed(json->states, json->states_internal, 1);
+    json->states.data[0] = JP_STATE_VALUE_BEGIN;
+    cx_array_init_fixed(json->vbuf, json->vbuf_internal, 0);
 }
 
 void cxJsonDestroy(CxJson *json) {
     cxBufferDestroy(&json->buffer);
-    if (json->states != json->states_internal) {
-        cxFreeDefault(json->states);
+    if (json->states.data != json->states_internal) {
+        cx_array_free(json->states);
     }
-    if (json->vbuf != json->vbuf_internal) {
-        cxFreeDefault(json->vbuf);
+    if (json->vbuf.data != json->vbuf_internal) {
+        cx_array_free(json->vbuf);
     }
     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) {
@@ -640,8 +578,9 @@
     if (cxBufferEof(&json->buffer)) {
         // reinitialize the buffer
         cxBufferDestroy(&json->buffer);
-        cxBufferInit(&json->buffer, (char*) buf, size,
-            NULL, CX_BUFFER_AUTO_EXTEND | CX_BUFFER_COPY_ON_WRITE);
+        if (buf == NULL) buf = ""; // buffer must not be initialized with NULL
+        cxBufferInit(&json->buffer, NULL, (char*) buf,
+                     size, CX_BUFFER_AUTO_EXTEND | CX_BUFFER_COPY_ON_WRITE);
         json->buffer.size = size;
         return 0;
     } else {
@@ -650,9 +589,9 @@
 }
 
 static void json_add_state(CxJson *json, int state) {
-    // we have guaranteed the necessary space with cx_array_simple_reserve()
+    // we have guaranteed the necessary space
     // therefore, we can safely add the state in the simplest way possible
-    json->states[json->states_size++] = state;
+    json->states.data[json->states.size++] = state;
 }
 
 #define return_rec(code) \
@@ -673,13 +612,21 @@
     }
 
     // pop the current state
-    assert(json->states_size > 0);
-    int state = json->states[--json->states_size];
+    assert(json->states.size > 0);
+    int state = json->states.data[--json->states.size];
 
-    // guarantee that at least two more states fit on the stack
-    CxArrayReallocator state_realloc = cx_array_reallocator(NULL, json->states_internal);
-    if (cx_array_simple_reserve_a(&state_realloc, json->states, 2)) {
-        return CX_JSON_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE
+    // guarantee that at least two more states fit into the array
+    const size_t required_states_depth = json->states.size + 2;
+    if (required_states_depth >= json->states.capacity) {
+        int alloc_error;
+        if (json->states.data == json->states_internal) {
+            alloc_error = cx_array_copy_to_new(json->states, required_states_depth);
+        } else {
+            alloc_error = cx_array_reserve(json->states, required_states_depth);
+        }
+        if (alloc_error) {
+            return CX_JSON_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE
+        }
     }
 
 
@@ -711,6 +658,16 @@
                 json_add_state(json, JP_STATE_OBJ_NAME_OR_CLOSE);
                 return_rec(CX_JSON_NO_ERROR);
             }
+            case CX_JSON_TOKEN_END_ARRAY: {
+                if (state == JP_STATE_VALUE_BEGIN_AR) {
+                    // discard the array from the value buffer
+                    json->vbuf.size--;
+                    json->states.size--;
+                    return_rec(CX_JSON_NO_ERROR);
+                } else {
+                    return_rec(CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN);
+                }
+            }
             case CX_JSON_TOKEN_STRING: {
                 if ((vbuf = json_create_value(json, CX_JSON_STRING)) == NULL) {
                     return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE
@@ -719,7 +676,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:
@@ -729,12 +686,13 @@
                     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)) {
-                        return_rec(CX_JSON_FORMAT_ERROR_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
                     }
                 }
                 return_rec(CX_JSON_NO_ERROR);
@@ -743,12 +701,12 @@
                 if ((vbuf = json_create_value(json, CX_JSON_LITERAL)) == NULL) {
                     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;
-                } else if (0 == cx_strcmp(cx_strcast(token.content), cx_str("false"))) {
-                    vbuf->value.literal = CX_JSON_FALSE;
+                if (0 == cx_strcmp(token.content, "true")) {
+                    vbuf->literal = CX_JSON_TRUE;
+                } else if (0 == cx_strcmp(token.content, "false")) {
+                    vbuf->literal = CX_JSON_FALSE;
                 } else {
-                    vbuf->value.literal = CX_JSON_NULL;
+                    vbuf->literal = CX_JSON_NULL;
                 }
                 return_rec(CX_JSON_NO_ERROR);
             }
@@ -763,7 +721,7 @@
             return_rec(CX_JSON_NO_ERROR);
         } else if (token.tokentype == CX_JSON_TOKEN_END_ARRAY) {
             // discard the array from the value buffer
-            json->vbuf_size--;
+            json->vbuf.size--;
             return_rec(CX_JSON_NO_ERROR);
         } else {
             return_rec(CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN);
@@ -771,7 +729,7 @@
     } else if (state == JP_STATE_OBJ_NAME_OR_CLOSE || state == JP_STATE_OBJ_NAME) {
         if (state == JP_STATE_OBJ_NAME_OR_CLOSE && token.tokentype == CX_JSON_TOKEN_END_OBJECT) {
             // discard the obj from the value buffer
-            json->vbuf_size--;
+            json->vbuf.size--;
             return_rec(CX_JSON_NO_ERROR);
         } else {
             // expect string
@@ -784,9 +742,9 @@
             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->vbuf_size > 0);
+            assert(json->uncompleted_member_name.ptr == NULL);
+            json->uncompleted_member_name = name;
+            assert(json->vbuf.size > 0);
 
             // next state
             json_add_state(json, JP_STATE_OBJ_COLON);
@@ -807,7 +765,7 @@
             return_rec(CX_JSON_NO_ERROR);
         } else if (token.tokentype == CX_JSON_TOKEN_END_OBJECT) {
             // discard the obj from the value buffer
-            json->vbuf_size--;
+            json->vbuf.size--;
             return_rec(CX_JSON_NO_ERROR);
         } else {
             return_rec(CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN);
@@ -815,34 +773,34 @@
     } else {
         // should be unreachable
         assert(false);
-        return_rec(-1);
+        return_rec(-1); // LCOV_EXCL_LINE
     }
 }
 
 CxJsonStatus cxJsonNext(CxJson *json, CxJsonValue **value) {
-    // check if buffer has been filled
+    // initialize output value
+    *value = &cx_json_value_nothing;
+
+    // check if the buffer has been filled
     if (json->buffer.space == NULL) {
         return CX_JSON_NULL_DATA;
     }
 
-    // initialize output value
-    *value = &cx_json_value_nothing;
-
     // parse data
     CxJsonStatus result;
     do {
         result = json_parse(json);
-        if (result == CX_JSON_NO_ERROR && json->states_size == 1) {
+        if (result == CX_JSON_NO_ERROR && json->states.size == 1) {
             // final state reached
-            assert(json->states[0] == JP_STATE_VALUE_END);
-            assert(json->vbuf_size == 0);
+            assert(json->states.data[0] == JP_STATE_VALUE_END);
+            assert(json->vbuf.size == 0);
 
             // write output value
             *value = json->parsed;
             json->parsed = NULL;
 
             // re-initialize state machine
-            json->states[0] = JP_STATE_VALUE_BEGIN;
+            json->states.data[0] = JP_STATE_VALUE_BEGIN;
 
             return CX_JSON_NO_ERROR;
         }
@@ -851,36 +809,60 @@
     // the parser might think there is no data
     // but when we did not reach the final state,
     // we know that there must be more to come
-    if (result == CX_JSON_NO_DATA && json->states_size > 1) {
+    if (result == CX_JSON_NO_DATA && json->states.size > 1) {
         return CX_JSON_INCOMPLETE_DATA;
     }
 
     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]);
+            for (size_t i = 0; i < value->array.size; i++) {
+                cxJsonValueFree(value->array.data[i]);
             }
-            cxFree(value->allocator, array.array);
+            cx_array_free_a(value->allocator, value->array);
             break;
         }
         case CX_JSON_STRING: {
-            cxFree(value->allocator, value->value.string.ptr);
+            cxFree(value->allocator, value->string.ptr);
             break;
         }
         default: {
@@ -896,15 +878,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
@@ -912,14 +887,23 @@
     return v;
 }
 
-CxJsonValue* cxJsonCreateArr(const CxAllocator* allocator) {
+CxJsonValue* cxJsonCreateArr(const CxAllocator* allocator, size_t capacity) {
     if (allocator == NULL) allocator = cxDefaultAllocator;
     CxJsonValue* v = cxMalloc(allocator, sizeof(CxJsonValue));
     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; }
+    if (capacity > 0) {
+        if (cx_array_init_a(allocator, v->array, capacity)) {
+            // LCOV_EXCL_START
+            cxFree(allocator, v);
+            return NULL;
+            // LCOV_EXCL_STOP
+        }
+    } else {
+        v->array.data = NULL;
+        v->array.size = v->array.capacity = 0;
+    }
     return v;
 }
 
@@ -929,7 +913,7 @@
     if (v == NULL) return NULL;
     v->allocator = allocator;
     v->type = CX_JSON_NUMBER;
-    v->value.number = num;
+    v->number = num;
     return v;
 }
 
@@ -939,15 +923,11 @@
     if (v == NULL) return NULL;
     v->allocator = allocator;
     v->type = CX_JSON_INTEGER;
-    v->value.integer = num;
+    v->integer = num;
     return v;
 }
 
-CxJsonValue* cxJsonCreateString(const CxAllocator* allocator, const char* str) {
-    return cxJsonCreateCxString(allocator, cx_str(str));
-}
-
-CxJsonValue* cxJsonCreateCxString(const CxAllocator* allocator, cxstring str) {
+CxJsonValue* cx_json_create_string(const CxAllocator* allocator, cxstring str) {
     if (allocator == NULL) allocator = cxDefaultAllocator;
     CxJsonValue* v = cxMalloc(allocator, sizeof(CxJsonValue));
     if (v == NULL) return NULL;
@@ -955,7 +935,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;
 }
 
@@ -965,7 +945,7 @@
     if (v == NULL) return NULL;
     v->allocator = allocator;
     v->type = CX_JSON_LITERAL;
-    v->value.literal = lit;
+    v->literal = lit;
     return v;
 }
 
@@ -1020,7 +1000,7 @@
     CxJsonValue** values = cxCallocDefault(count, sizeof(CxJsonValue*));
     if (values == NULL) return -1;
     for (size_t i = 0; i < count; i++) {
-        values[i] = cxJsonCreateCxString(arr->allocator, str[i]);
+        values[i] = cxJsonCreateString(arr->allocator, str[i]);
         if (values[i] == NULL) { json_arr_free_temp(values, count); return -1; }
     }
     int ret = cxJsonArrAddValues(arr, values, count);
@@ -1041,70 +1021,50 @@
 }
 
 int cxJsonArrAddValues(CxJsonValue* arr, CxJsonValue* const* val, size_t count) {
-    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,
-            val, count
-    );
+    return cx_array_add_array_a(arr->allocator, arr->array, val, count);
 }
 
-int cxJsonObjPut(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)) {
-        cx_strfree_a(obj->allocator, &k);
-        return 1;
-    } else {
-        return 0;
-    }
+int cx_json_obj_put(CxJsonValue* obj, cxstring name, CxJsonValue* child) {
+    return cxMapPut(obj->object, name, child);
 }
 
-CxJsonValue* cxJsonObjPutObj(CxJsonValue* obj, cxstring name) {
+CxJsonValue* cx_json_obj_put_obj(CxJsonValue* obj, cxstring name) {
     CxJsonValue* v = cxJsonCreateObj(obj->allocator);
     if (v == NULL) return NULL;
     if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL; }
     return v;
 }
 
-CxJsonValue* cxJsonObjPutArr(CxJsonValue* obj, cxstring name) {
-    CxJsonValue* v = cxJsonCreateArr(obj->allocator);
+CxJsonValue* cx_json_obj_put_arr(CxJsonValue* obj, cxstring name, size_t capacity) {
+    CxJsonValue* v = cxJsonCreateArr(obj->allocator, capacity);
     if (v == NULL) return NULL;
     if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL; }
     return v;
 }
 
-CxJsonValue* cxJsonObjPutNumber(CxJsonValue* obj, cxstring name, double num) {
+CxJsonValue* cx_json_obj_put_number(CxJsonValue* obj, cxstring name, double num) {
     CxJsonValue* v = cxJsonCreateNumber(obj->allocator, num);
     if (v == NULL) return NULL;
     if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL; }
     return v;
 }
 
-CxJsonValue* cxJsonObjPutInteger(CxJsonValue* obj, cxstring name, int64_t num) {
+CxJsonValue* cx_json_obj_put_integer(CxJsonValue* obj, cxstring name, int64_t num) {
     CxJsonValue* v = cxJsonCreateInteger(obj->allocator, num);
     if (v == NULL) return NULL;
     if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL; }
     return v;
 }
 
-CxJsonValue* cxJsonObjPutString(CxJsonValue* obj, cxstring name, const char* str) {
+CxJsonValue* cx_json_obj_put_string(CxJsonValue* obj, cxstring name, cxstring str) {
     CxJsonValue* v = cxJsonCreateString(obj->allocator, str);
     if (v == NULL) return NULL;
     if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL; }
     return v;
 }
 
-CxJsonValue* cxJsonObjPutCxString(CxJsonValue* obj, cxstring name, cxstring str) {
-    CxJsonValue* v = cxJsonCreateCxString(obj->allocator, str);
-    if (v == NULL) return NULL;
-    if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL; }
-    return v;
-}
-
-CxJsonValue* cxJsonObjPutLiteral(CxJsonValue* obj, cxstring name, CxJsonLiteral lit) {
+CxJsonValue* cx_json_obj_put_literal(CxJsonValue* obj, cxstring name, CxJsonLiteral lit) {
     CxJsonValue* v = cxJsonCreateLiteral(obj->allocator, lit);
     if (v == NULL) return NULL;
     if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL;}
@@ -1112,98 +1072,75 @@
 }
 
 CxJsonValue *cxJsonArrGet(const CxJsonValue *value, size_t index) {
-    if (index >= value->value.array.array_size) {
+    if (index >= value->array.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.size) {
         return NULL;
     }
-    CxJsonValue *ret = value->value.array.array[index];
-    // TODO: replace with a low level cx_array_remove()
-    size_t count = value->value.array.array_size - index - 1;
-    if (count > 0) {
-        memmove(value->value.array.array + index, value->value.array.array + index + 1, count * sizeof(CxJsonValue*));
-    }
-    value->value.array.array_size--;
+    CxJsonValue *ret = value->array.data[index];
+    cx_array_remove(value->array, index);
     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,
-        true // arrays need to keep order
-    );
+    return cx_array_iterator_ptr(value->array);
 }
 
-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,
@@ -1214,7 +1151,6 @@
 CxJsonWriter cxJsonWriterPretty(bool use_spaces) {
     return (CxJsonWriter) {
         true,
-        true,
         6,
         use_spaces,
         4,
@@ -1279,17 +1215,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];
-                if (settings->sort_members) {
-                    depth++;depth--;
-                }
-
+            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)) {
@@ -1299,26 +1226,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);
@@ -1350,7 +1278,9 @@
                 if (cx_json_write_rec(
                         target, element,
                         wfunc, settings, depth)
-                ) return 1;
+                ) {
+                    return 1; // LCOV_EXCL_LINE
+                }
 
                 if (iter.index < iter.elem_count - 1) {
                     const char *arr_value_sep = ", ";
@@ -1369,13 +1299,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: {
@@ -1383,7 +1314,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 ,
@@ -1447,17 +1378,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 {
@@ -1495,3 +1426,144 @@
     }
     return cx_json_write_rec(target, value, wfunc, settings, 0);
 }
+
+static cxmutstr cx_json_to_string(CxJsonValue *value, const CxAllocator *allocator, CxJsonWriter *writer) {
+    if (allocator == NULL) allocator = cxDefaultAllocator;
+    CxBuffer buffer;
+    if (cxBufferInit(&buffer, allocator, NULL, 128,
+                     CX_BUFFER_AUTO_EXTEND | CX_BUFFER_DO_NOT_FREE)) {
+        return (cxmutstr){NULL, 0};
+    }
+    if (cx_json_write_rec(&buffer, value, cxBufferWriteFunc, writer, 0)
+            || cxBufferTerminate(&buffer)) {
+        // LCOV_EXCL_START
+        buffer.flags &= ~CX_BUFFER_DO_NOT_FREE;
+        cxBufferDestroy(&buffer);
+        return (cxmutstr){NULL, 0};
+        // LCOV_EXCL_STOP
+    } else {
+        cxmutstr str = cx_mutstrn(buffer.space, buffer.size);
+        cxBufferDestroy(&buffer);
+        return str;
+    }
+
+}
+
+cxmutstr cxJsonToString(const CxAllocator *allocator, CxJsonValue *value) {
+    CxJsonWriter writer = cxJsonWriterCompact();
+    return cx_json_to_string(value, allocator, &writer);
+}
+
+cxmutstr cxJsonToPrettyString(const CxAllocator *allocator, CxJsonValue *value) {
+    CxJsonWriter writer = cxJsonWriterPretty(true);
+    return cx_json_to_string(value, allocator, &writer);
+}
+
+int cxJsonCompare(const CxJsonValue *json, const CxJsonValue *other) {
+    if (json == other) return 0;
+    if (json == NULL || other == NULL) return -1;
+    if (json->type != other->type) {
+        if (!cxJsonIsNumber(json)) return -1;
+        if (!cxJsonIsNumber(other)) return -1;
+    }
+    switch (json->type) {
+        case CX_JSON_NOTHING:
+            return 0;
+        case CX_JSON_OBJECT:
+            return cxMapCompare(json->object, other->object);
+        case CX_JSON_ARRAY:
+            if (json->array.size != other->array.size) return -1;
+            for (size_t i = 0; i < json->array.size; i++) {
+                const int d = cxJsonCompare(json->array.data[i], other->array.data[i]);
+                if (d != 0) return d;
+            }
+            return 0;
+        case CX_JSON_STRING:
+            return cx_strcmp(json->string, other->string);
+        case CX_JSON_INTEGER:
+            if (other->type == CX_JSON_INTEGER) {
+                return cx_vcmp_int64(json->integer, other->integer);
+            } else {
+                return cx_vcmp_double(cxJsonAsDouble(json), other->number);
+            }
+        case CX_JSON_NUMBER:
+            return cx_vcmp_double(json->number, cxJsonAsDouble(other));
+        case CX_JSON_LITERAL:
+            return json->literal == other->literal ? 0 : -1;
+        default:
+            // LCOV_EXCL_START
+            // unreachable
+            assert(false);
+            return -1;
+            // LCOV_EXCL_STOP
+    }
+}
+
+CxJsonValue* cxJsonClone(const CxJsonValue* value, const CxAllocator* allocator) {
+    return cx_json_clone_func(NULL, value, allocator, NULL);
+}
+
+CxJsonValue* cx_json_clone_func(CxJsonValue* target, const CxJsonValue* source,
+        const CxAllocator* allocator, cx_attr_unused void *data) {
+    if (source == NULL || source->type == CX_JSON_NOTHING) {
+        return &cx_json_value_nothing;
+    }
+    if (allocator == NULL) allocator = cxDefaultAllocator;
+
+#define return_value(v) { \
+        CxJsonValue *ret = v; \
+        if (target == NULL) { \
+            return ret; \
+        } else { \
+            *target = *ret; \
+            cxFree(allocator, ret); \
+            return target; \
+        } \
+    }
+
+    switch (source->type) {
+        case CX_JSON_OBJECT: {
+            CxJsonValue *obj = cxJsonCreateObj(allocator);
+            if (obj == NULL) return NULL; // LCOV_EXCL_LINE
+            if (cxMapClone(obj->object, source->object, cxJsonCloneFunc, allocator, NULL)) {
+                // LCOV_EXCL_START
+                cxJsonValueFree(obj);
+                return NULL;
+                // LCOV_EXCL_STOP
+            }
+            return_value(obj);
+        }
+        case CX_JSON_ARRAY: {
+            const size_t elem_count = source->array.size;
+            CxJsonValue *arr = cxJsonCreateArr(allocator, elem_count);
+            if (arr == NULL) return NULL; // LCOV_EXCL_LINE
+            arr->array.size = elem_count;
+            for (size_t i = 0 ; i < elem_count ; i++) {
+                CxJsonValue *e = cx_json_clone_func(NULL, source->array.data[i], allocator, NULL);
+                if (e == NULL) {
+                    // LCOV_EXCL_START
+                    cxJsonValueFree(arr);
+                    return NULL;
+                    // LCOV_EXCL_STOP
+                }
+                arr->array.data[i] = e;
+            }
+            return_value(arr);
+        }
+        case CX_JSON_STRING:
+            return_value(cxJsonCreateString(allocator, source->string));
+        case CX_JSON_INTEGER:
+            return_value(cxJsonCreateInteger(allocator, source->integer));
+        case CX_JSON_NUMBER:
+            return_value(cxJsonCreateNumber(allocator, source->number));
+        case CX_JSON_LITERAL:
+            return_value(cxJsonCreateLiteral(allocator, source->literal));
+        default:
+            // LCOV_EXCL_START
+            // unreachable
+            assert(false);
+            return NULL;
+            // LCOV_EXCL_STOP
+    }
+#undef return_value
+}

mercurial