--- 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 {