Fri, 23 May 2025 12:44:24 +0200
make test-compile depend on both static and shared
the shared lib is not needed for the tests,
but when run with coverage, gcov will be confused
when outdated line information is available from
a previous shared build
/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2024 Mike Becker, Olaf Wintermann All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include "cx/json.h" #include <string.h> #include <assert.h> #include <stdio.h> #include <inttypes.h> /* * RFC 8259 * https://tools.ietf.org/html/rfc8259 */ 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 CxJsonObjValue *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); size_t index = cx_array_binary_search( obj->value.object.values, obj->value.object.values_size, sizeof(CxJsonObjValue), &kv_dummy, json_cmp_objvalue ); if (index == obj->value.object.values_size) { return NULL; } else { return &obj->value.object.values[index]; } } 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); } } static bool json_isdigit(char c) { // TODO: remove once UCX has public API for this return c >= '0' && c <= '9'; } static bool json_isspace(char c) { // TODO: remove once UCX has public API for this return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\v' || c == '\f'; } static int num_isexp(const char *content, size_t length, size_t pos) { if (pos >= length) { return 0; } int ok = 0; for (size_t i = pos; i < length; i++) { char c = content[i]; if (json_isdigit(c)) { ok = 1; } else if (i == pos) { if (!(c == '+' || c == '-')) { return 0; } } else { return 0; } } return ok; } static CxJsonTokenType token_numbertype(const char *content, size_t length) { if (length == 0) return CX_JSON_TOKEN_ERROR; if (content[0] != '-' && !json_isdigit(content[0])) { return CX_JSON_TOKEN_ERROR; } CxJsonTokenType type = CX_JSON_TOKEN_INTEGER; for (size_t i = 1; i < length; i++) { if (content[i] == '.') { if (type == CX_JSON_TOKEN_NUMBER) { return CX_JSON_TOKEN_ERROR; // more than one decimal separator } type = CX_JSON_TOKEN_NUMBER; } else if (content[i] == 'e' || content[i] == 'E') { return num_isexp(content, length, i + 1) ? CX_JSON_TOKEN_NUMBER : CX_JSON_TOKEN_ERROR; } else if (!json_isdigit(content[i])) { return CX_JSON_TOKEN_ERROR; // char is not a digit, decimal separator or exponent sep } } return type; } 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) { 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 } json->uncompleted = (CxJsonToken){0}; CxJsonTokenType ttype; if (isstring) { 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"))) { ttype = CX_JSON_TOKEN_LITERAL; } else { ttype = token_numbertype(str.ptr, str.length); } } if (ttype == CX_JSON_TOKEN_ERROR) { if (allocated) { cx_strfree(&str); } return (CxJsonToken){CX_JSON_TOKEN_ERROR, false, {NULL, 0}}; } return (CxJsonToken){ttype, allocated, str}; } static CxJsonTokenType char2ttype(char c) { switch (c) { case '[': { return CX_JSON_TOKEN_BEGIN_ARRAY; } case '{': { return CX_JSON_TOKEN_BEGIN_OBJECT; } case ']': { return CX_JSON_TOKEN_END_ARRAY; } case '}': { return CX_JSON_TOKEN_END_OBJECT; } case ':': { return CX_JSON_TOKEN_NAME_SEPARATOR; } case ',': { return CX_JSON_TOKEN_VALUE_SEPARATOR; } case '"': { return CX_JSON_TOKEN_STRING; } default: { if (json_isspace(c)) { return CX_JSON_TOKEN_SPACE; } } } return CX_JSON_NO_TOKEN; } 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 ? CX_JSON_NO_DATA : CX_JSON_INCOMPLETE_DATA; } // current token type and start index 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] == '\\'; for (size_t i = json->buffer.pos; i < json->buffer.size; i++) { char c = json->buffer.space[i]; if (ttype != CX_JSON_TOKEN_STRING) { // currently non-string token CxJsonTokenType ctype = char2ttype(c); // start of new token? if (ttype == CX_JSON_NO_TOKEN) { if (ctype == CX_JSON_TOKEN_SPACE) { json->buffer.pos++; continue; } else if (ctype == CX_JSON_TOKEN_STRING) { // begin string ttype = CX_JSON_TOKEN_STRING; token_part_start = i; } else if (ctype != CX_JSON_NO_TOKEN) { // single-char token json->buffer.pos = i + 1; *result = (CxJsonToken){ctype, false, {NULL, 0}}; return CX_JSON_NO_ERROR; } else { ttype = CX_JSON_TOKEN_LITERAL; // number or literal token_part_start = i; } } else { // finish token if (ctype != CX_JSON_NO_TOKEN) { *result = token_create(json, false, token_part_start, i); if (result->tokentype == CX_JSON_NO_TOKEN) { return CX_JSON_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE } if (result->tokentype == CX_JSON_TOKEN_ERROR) { return CX_JSON_FORMAT_ERROR_NUMBER; } json->buffer.pos = i; return CX_JSON_NO_ERROR; } } } else { // currently inside a string if (escape_end_of_string) { escape_end_of_string = false; } else { if (c == '"') { *result = token_create(json, true, token_part_start, i + 1); if (result->tokentype == CX_JSON_NO_TOKEN) { return CX_JSON_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE } json->buffer.pos = i + 1; return CX_JSON_NO_ERROR; } else if (c == '\\') { escape_end_of_string = true; } } } } if (ttype != CX_JSON_NO_TOKEN) { // 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) { return CX_JSON_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE } json->uncompleted = uncompleted; } 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) { 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; } return CX_JSON_INCOMPLETE_DATA; } // converts a Unicode codepoint to utf8 static unsigned codepoint_to_utf8(uint32_t codepoint, char *output_buf) { if (codepoint <= 0x7F) { *output_buf = (char)codepoint; return 1; } else if (codepoint <= 0x7FF) { output_buf[0] = (char)(0xC0 | ((codepoint >> 6) & 0x1F)); output_buf[1] = (char)(0x80 | (codepoint & 0x3F)); return 2; } else if (codepoint <= 0xFFFF) { output_buf[0] = (char)(0xE0 | ((codepoint >> 12) & 0x0F)); output_buf[1] = (char)(0x80 | ((codepoint >> 6) & 0x3F)); output_buf[2] = (char)(0x80 | (codepoint & 0x3F)); return 3; } else if (codepoint <= 0x10FFFF) { output_buf[0] = (char)(0xF0 | ((codepoint >> 18) & 0x07)); output_buf[1] = (char)(0x80 | ((codepoint >> 12) & 0x3F)); output_buf[2] = (char)(0x80 | ((codepoint >> 6) & 0x3F)); output_buf[3] = (char)(0x80 | (codepoint & 0x3F)); return 4; } return 0; // LCOV_EXCL_LINE } // converts a utf16 surrogate pair to utf8 static inline uint32_t utf16pair_to_codepoint(uint16_t c0, uint16_t c1) { return ((c0 - 0xD800) << 10) + (c1 - 0xDC00) + 0x10000; } static unsigned unescape_unicode_string(cxstring str, char *utf8buf) { // str is supposed to start with "\uXXXX" or "\uXXXX\uXXXX" // remaining bytes in the string are ignored (str may be larger!) if (str.length < 6 || str.ptr[0] != '\\' || str.ptr[1] != 'u') { return 0; } unsigned utf8len = 0; cxstring ustr1 = { str.ptr + 2, 4}; uint16_t utf16a, utf16b; if (!cx_strtou16_lc(ustr1, &utf16a, 16, "")) { uint32_t codepoint; if (utf16a < 0xD800 || utf16a > 0xE000) { // character is in the Basic Multilingual Plane // and encoded as a single utf16 char codepoint = utf16a; utf8len = codepoint_to_utf8(codepoint, utf8buf); } else if (utf16a >= 0xD800 && utf16a <= 0xDBFF) { // character is encoded as a surrogate pair // get next 6 bytes if (str.length >= 12) { if (str.ptr[6] == '\\' && str.ptr[7] == 'u') { cxstring ustr2 = { str.ptr+8, 4 }; if (!cx_strtou16_lc(ustr2, &utf16b, 16, "") && utf16b >= 0xDC00 && utf16b <= 0xDFFF) { codepoint = utf16pair_to_codepoint(utf16a, utf16b); utf8len = codepoint_to_utf8(codepoint, utf8buf); } } } } } return utf8len; } static cxmutstr unescape_string(const CxAllocator *a, cxmutstr str) { // note: this function expects that str contains the enclosing quotes! cxmutstr result; result.length = 0; result.ptr = cxMalloc(a, str.length - 1); if (result.ptr == NULL) return result; // LCOV_EXCL_LINE bool u = false; for (size_t i = 1; i < str.length - 1; i++) { char c = str.ptr[i]; if (u) { u = false; if (c == 'n') { c = '\n'; } else if (c == '"') { c = '"'; } else if (c == 't') { c = '\t'; } else if (c == 'r') { c = '\r'; } else if (c == '\\') { c = '\\'; } else if (c == '/') { c = '/'; // always unescape, we don't need settings here } else if (c == 'f') { c = '\f'; } else if (c == 'b') { c = '\b'; } else if (c == 'u') { char utf8buf[4]; unsigned utf8len = unescape_unicode_string( cx_strn(str.ptr + i - 1, str.length + 1 - i), utf8buf ); if(utf8len > 0) { i += utf8len < 4 ? 4 : 10; // add all bytes from utf8buf except the last char // to the result (last char will be added below) utf8len--; c = utf8buf[utf8len]; for (unsigned x = 0; x < utf8len; x++) { result.ptr[result.length++] = utf8buf[x]; } } else { // decoding failed, ignore the entire sequence result.ptr[result.length++] = '\\'; } } 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++] = c; } else { if (c == '\\') { u = true; } else { result.ptr[result.length++] = c; } } } result.ptr[result.length] = 0; return result; } static cxmutstr escape_string(cxmutstr 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}; bool all_printable = true; for (size_t i = 0; i < str.length; i++) { unsigned char c = str.ptr[i]; bool escape = c < 0x20 || c == '\\' || c == '"' || (escape_slash && c == '/'); if (all_printable && escape) { 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); cxBufferWrite(str.ptr, 1, i, &buf); all_printable = false; } if (escape) { cxBufferPut(&buf, '\\'); if (c == '\"') { cxBufferPut(&buf, '\"'); } else if (c == '\n') { cxBufferPut(&buf, 'n'); } else if (c == '\t') { cxBufferPut(&buf, 't'); } else if (c == '\r') { cxBufferPut(&buf, 'r'); } else if (c == '\\') { cxBufferPut(&buf, '\\'); } else if (c == '/') { cxBufferPut(&buf, '/'); } else if (c == '\f') { cxBufferPut(&buf, 'f'); } else if (c == '\b') { cxBufferPut(&buf, 'b'); } else { char code[6]; snprintf(code, sizeof(code), "u%04x", (unsigned int) c); cxBufferPutString(&buf, code); } } else if (!all_printable) { cxBufferPut(&buf, c); } } if (!all_printable) { str = cx_mutstrn(buf.space, buf.size); } cxBufferDestroy(&buf); return str; } static CxJsonValue* json_create_value(CxJson *json, CxJsonValueType type) { CxJsonValue *v = cxCalloc(json->allocator, 1, sizeof(CxJsonValue)); if (v == NULL) return NULL; // LCOV_EXCL_LINE // initialize the value 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 } 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 } // add the new value to a possible parent if (json->vbuf_size > 0) { CxJsonValue *parent = json->vbuf[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)) { 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)) { goto create_json_value_exit_error; // LCOV_EXCL_LINE } json->uncompleted_member.name = (cxmutstr) {NULL, 0}; } else { assert(false); // LCOV_EXCL_LINE } } // 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 currently no value is parsed, this is now the value of interest if (json->parsed == NULL) { json->parsed = v; } return v; // LCOV_EXCL_START create_json_value_exit_error: cxJsonValueFree(v); return NULL; // LCOV_EXCL_STOP } #define JP_STATE_VALUE_BEGIN 0 #define JP_STATE_VALUE_END 10 #define JP_STATE_VALUE_BEGIN_OBJ 1 #define JP_STATE_OBJ_SEP_OR_CLOSE 11 #define JP_STATE_VALUE_BEGIN_AR 2 #define JP_STATE_ARRAY_SEP_OR_CLOSE 12 #define JP_STATE_OBJ_NAME_OR_CLOSE 5 #define JP_STATE_OBJ_NAME 6 #define JP_STATE_OBJ_COLON 7 void cxJsonInit(CxJson *json, const CxAllocator *allocator) { if (allocator == NULL) { allocator = cxDefaultAllocator; } 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); } void cxJsonDestroy(CxJson *json) { cxBufferDestroy(&json->buffer); if (json->states != json->states_internal) { cxFreeDefault(json->states); } if (json->vbuf != json->vbuf_internal) { cxFreeDefault(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}; } } int cxJsonFilln(CxJson *json, const char *buf, size_t size) { 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); json->buffer.size = size; return 0; } else { return size != cxBufferAppend(buf, 1, size, &json->buffer); } } static void json_add_state(CxJson *json, int state) { // we have guaranteed the necessary space with cx_array_simple_reserve() // therefore, we can safely add the state in the simplest way possible json->states[json->states_size++] = state; } #define return_rec(code) \ token_destroy(&token); \ return code static enum cx_json_status json_parse(CxJson *json) { // Reserve a pointer for a possibly read value CxJsonValue *vbuf = NULL; // grab the next token CxJsonToken token; { enum cx_json_status ret = token_parse_next(json, &token); if (ret != CX_JSON_NO_ERROR) { return ret; } } // pop the current state assert(json->states_size > 0); int state = json->states[--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 } // 0 JP_STATE_VALUE_BEGIN value begin // 10 JP_STATE_VALUE_END expect value end // 1 JP_STATE_VALUE_BEGIN_OBJ value begin (inside object) // 11 JP_STATE_OBJ_SEP_OR_CLOSE object, expect separator, objclose // 2 JP_STATE_VALUE_BEGIN_AR value begin (inside array) // 12 JP_STATE_ARRAY_SEP_OR_CLOSE array, expect separator or arrayclose // 5 JP_STATE_OBJ_NAME_OR_CLOSE object, expect name or objclose // 6 JP_STATE_OBJ_NAME object, expect name // 7 JP_STATE_OBJ_COLON object, expect ':' if (state < 3) { // push expected end state to the stack json_add_state(json, 10 + state); switch (token.tokentype) { case CX_JSON_TOKEN_BEGIN_ARRAY: { if (json_create_value(json, CX_JSON_ARRAY) == NULL) { return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE } json_add_state(json, JP_STATE_VALUE_BEGIN_AR); return_rec(CX_JSON_NO_ERROR); } case CX_JSON_TOKEN_BEGIN_OBJECT: { if (json_create_value(json, CX_JSON_OBJECT) == NULL) { return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE } json_add_state(json, JP_STATE_OBJ_NAME_OR_CLOSE); return_rec(CX_JSON_NO_ERROR); } 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 } cxmutstr str = unescape_string(json->allocator, token.content); if (str.ptr == NULL) { return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE } vbuf->value.string = str; return_rec(CX_JSON_NO_ERROR); } case CX_JSON_TOKEN_INTEGER: case CX_JSON_TOKEN_NUMBER: { int type = token.tokentype == CX_JSON_TOKEN_INTEGER ? CX_JSON_INTEGER : CX_JSON_NUMBER; if (NULL == (vbuf = json_create_value(json, type))) { return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE } if (type == CX_JSON_INTEGER) { if (cx_strtoi64(token.content, &vbuf->value.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); } } return_rec(CX_JSON_NO_ERROR); } case CX_JSON_TOKEN_LITERAL: { 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; } else { vbuf->value.literal = CX_JSON_NULL; } return_rec(CX_JSON_NO_ERROR); } default: { return_rec(CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN); } } } else if (state == JP_STATE_ARRAY_SEP_OR_CLOSE) { // expect ',' or ']' if (token.tokentype == CX_JSON_TOKEN_VALUE_SEPARATOR) { json_add_state(json, JP_STATE_VALUE_BEGIN_AR); 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--; return_rec(CX_JSON_NO_ERROR); } else { return_rec(CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN); } } 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--; return_rec(CX_JSON_NO_ERROR); } else { // expect string if (token.tokentype != CX_JSON_TOKEN_STRING) { return_rec(CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN); } // add new entry cxmutstr name = unescape_string(json->allocator, token.content); 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); // next state json_add_state(json, JP_STATE_OBJ_COLON); return_rec(CX_JSON_NO_ERROR); } } else if (state == JP_STATE_OBJ_COLON) { // expect ':' if (token.tokentype != CX_JSON_TOKEN_NAME_SEPARATOR) { return_rec(CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN); } // next state json_add_state(json, JP_STATE_VALUE_BEGIN_OBJ); return_rec(CX_JSON_NO_ERROR); } else if (state == JP_STATE_OBJ_SEP_OR_CLOSE) { // expect ',' or '}' if (token.tokentype == CX_JSON_TOKEN_VALUE_SEPARATOR) { json_add_state(json, JP_STATE_OBJ_NAME); 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--; return_rec(CX_JSON_NO_ERROR); } else { return_rec(CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN); } } else { // should be unreachable assert(false); return_rec(-1); } } CxJsonStatus cxJsonNext(CxJson *json, CxJsonValue **value) { // check if 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) { // final state reached assert(json->states[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; return CX_JSON_NO_ERROR; } } while (result == CX_JSON_NO_ERROR); // 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) { return CX_JSON_INCOMPLETE_DATA; } return result; } 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); break; } case CX_JSON_ARRAY: { CxJsonArray array = value->value.array; for (size_t i = 0; i < array.array_size; i++) { cxJsonValueFree(array.array[i]); } cxFree(value->allocator, array.array); break; } case CX_JSON_STRING: { cxFree(value->allocator, value->value.string.ptr); break; } default: { break; } } cxFree(value->allocator, value); } CxJsonValue* cxJsonCreateObj(const CxAllocator* allocator) { if (allocator == NULL) allocator = cxDefaultAllocator; CxJsonValue* v = cxMalloc(allocator, sizeof(CxJsonValue)); 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); cxFree(allocator, v); return NULL; // LCOV_EXCL_STOP } return v; } CxJsonValue* cxJsonCreateArr(const CxAllocator* allocator) { 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; } return v; } CxJsonValue* cxJsonCreateNumber(const CxAllocator* allocator, double num) { if (allocator == NULL) allocator = cxDefaultAllocator; CxJsonValue* v = cxMalloc(allocator, sizeof(CxJsonValue)); if (v == NULL) return NULL; v->allocator = allocator; v->type = CX_JSON_NUMBER; v->value.number = num; return v; } CxJsonValue* cxJsonCreateInteger(const CxAllocator* allocator, int64_t num) { if (allocator == NULL) allocator = cxDefaultAllocator; CxJsonValue* v = cxMalloc(allocator, sizeof(CxJsonValue)); if (v == NULL) return NULL; v->allocator = allocator; v->type = CX_JSON_INTEGER; v->value.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) { if (allocator == NULL) allocator = cxDefaultAllocator; CxJsonValue* v = cxMalloc(allocator, sizeof(CxJsonValue)); if (v == NULL) return NULL; v->allocator = allocator; 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; return v; } CxJsonValue* cxJsonCreateLiteral(const CxAllocator* allocator, CxJsonLiteral lit) { if (allocator == NULL) allocator = cxDefaultAllocator; CxJsonValue* v = cxMalloc(allocator, sizeof(CxJsonValue)); if (v == NULL) return NULL; v->allocator = allocator; v->type = CX_JSON_LITERAL; v->value.literal = lit; return v; } // LCOV_EXCL_START // never called as long as malloc() does not return NULL static void json_arr_free_temp(CxJsonValue** values, size_t count) { for (size_t i = 0; i < count; i++) { if (values[i] == NULL) break; cxJsonValueFree(values[i]); } cxFreeDefault(values); } // LCOV_EXCL_STOP int cxJsonArrAddNumbers(CxJsonValue* arr, const double* num, size_t count) { CxJsonValue** values = cxCallocDefault(count, sizeof(CxJsonValue*)); if (values == NULL) return -1; for (size_t i = 0; i < count; i++) { values[i] = cxJsonCreateNumber(arr->allocator, num[i]); if (values[i] == NULL) { json_arr_free_temp(values, count); return -1; } } int ret = cxJsonArrAddValues(arr, values, count); cxFreeDefault(values); return ret; } int cxJsonArrAddIntegers(CxJsonValue* arr, const int64_t* num, size_t count) { CxJsonValue** values = cxCallocDefault(count, sizeof(CxJsonValue*)); if (values == NULL) return -1; for (size_t i = 0; i < count; i++) { values[i] = cxJsonCreateInteger(arr->allocator, num[i]); if (values[i] == NULL) { json_arr_free_temp(values, count); return -1; } } int ret = cxJsonArrAddValues(arr, values, count); cxFreeDefault(values); return ret; } int cxJsonArrAddStrings(CxJsonValue* arr, const char* const* str, size_t count) { CxJsonValue** values = cxCallocDefault(count, sizeof(CxJsonValue*)); if (values == NULL) return -1; for (size_t i = 0; i < count; 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); cxFreeDefault(values); return ret; } int cxJsonArrAddCxStrings(CxJsonValue* arr, const cxstring* str, size_t count) { 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]); if (values[i] == NULL) { json_arr_free_temp(values, count); return -1; } } int ret = cxJsonArrAddValues(arr, values, count); cxFreeDefault(values); return ret; } int cxJsonArrAddLiterals(CxJsonValue* arr, const CxJsonLiteral* lit, size_t count) { CxJsonValue** values = cxCallocDefault(count, sizeof(CxJsonValue*)); if (values == NULL) return -1; for (size_t i = 0; i < count; i++) { values[i] = cxJsonCreateLiteral(arr->allocator, lit[i]); if (values[i] == NULL) { json_arr_free_temp(values, count); return -1; } } int ret = cxJsonArrAddValues(arr, values, count); cxFreeDefault(values); return ret; } 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 ); } 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; } } CxJsonValue* cxJsonObjPutObj(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); 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* 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* 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* 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* v = cxJsonCreateLiteral(obj->allocator, lit); if (v == NULL) return NULL; if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL;} return v; } CxJsonValue *cxJsonArrGet(const CxJsonValue *value, size_t index) { if (index >= value->value.array.array_size) { return &cx_json_value_nothing; } return value->value.array.array[index]; } CxIterator cxJsonArrIter(const CxJsonValue *value) { return cxIteratorPtr( value->value.array.array, value->value.array.array_size ); } CxIterator cxJsonObjIter(const CxJsonValue *value) { return cxIterator( value->value.object.values, sizeof(CxJsonObjValue), value->value.object.values_size ); } CxJsonValue *cx_json_obj_get_cxstr(const CxJsonValue *value, cxstring name) { CxJsonObjValue *member = json_find_objvalue(value, name); if (member == NULL) { return &cx_json_value_nothing; } else { return member->value; } } CxJsonWriter cxJsonWriterCompact(void) { return (CxJsonWriter) { false, true, 6, false, 4, false }; } CxJsonWriter cxJsonWriterPretty(bool use_spaces) { return (CxJsonWriter) { true, true, 6, use_spaces, 4, false }; } static int cx_json_writer_indent( void *target, cx_write_func wfunc, const CxJsonWriter *settings, unsigned int depth ) { if (depth == 0) return 0; // determine the width and characters to use const char* indent; // for 32 prepared chars size_t width = depth; if (settings->indent_space) { if (settings->indent == 0) return 0; width *= settings->indent; indent = " "; } else { indent = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"; } // calculate the number of write calls and write size_t full = width / 32; size_t remaining = width % 32; for (size_t i = 0; i < full; i++) { if (32 != wfunc(indent, 1, 32, target)) return 1; } if (remaining != wfunc(indent, 1, remaining, target)) return 1; return 0; } int cx_json_write_rec( void *target, const CxJsonValue *value, cx_write_func wfunc, const CxJsonWriter *settings, unsigned int depth ) { // keep track of written items // the idea is to reduce the number of jumps for error checking size_t actual = 0, expected = 0; // small buffer for number to string conversions char numbuf[40]; // recursively write the values switch (value->type) { case CX_JSON_OBJECT: { const char *begin_obj = "{\n"; if (settings->pretty) { actual += wfunc(begin_obj, 1, 2, target); expected += 2; } else { actual += wfunc(begin_obj, 1, 1, target); 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--; } // possible indentation if (settings->pretty) { if (cx_json_writer_indent(target, wfunc, settings, depth)) { return 1; // LCOV_EXCL_LINE } } // the name actual += wfunc("\"", 1, 1, target); cxmutstr name = escape_string(member->name, 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; } else { actual += wfunc(obj_name_sep, 1, 1, target); expected += 3 + member->name.length; } // 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) { const char *obj_value_sep = ",\n"; if (settings->pretty) { actual += wfunc(obj_value_sep, 1, 2, target); expected += 2; } else { actual += wfunc(obj_value_sep, 1, 1, target); expected++; } } else { if (settings->pretty) { actual += wfunc("\n", 1, 1, target); expected ++; } } } depth--; if (settings->pretty) { if (cx_json_writer_indent(target, wfunc, settings, depth)) return 1; } actual += wfunc("}", 1, 1, target); expected++; break; } case CX_JSON_ARRAY: { actual += wfunc("[", 1, 1, target); expected++; CxIterator iter = cxJsonArrIter(value); cx_foreach(CxJsonValue*, element, iter) { if (cx_json_write_rec( target, element, wfunc, settings, depth) ) return 1; if (iter.index < iter.elem_count - 1) { const char *arr_value_sep = ", "; if (settings->pretty) { actual += wfunc(arr_value_sep, 1, 2, target); expected += 2; } else { actual += wfunc(arr_value_sep, 1, 1, target); expected++; } } } actual += wfunc("]", 1, 1, target); expected++; break; } case CX_JSON_STRING: { actual += wfunc("\"", 1, 1, target); cxmutstr str = escape_string(value->value.string, settings->escape_slash); actual += wfunc(str.ptr, 1, str.length, target); if (str.ptr != value->value.string.ptr) { cx_strfree(&str); } actual += wfunc("\"", 1, 1, target); expected += 2 + value->value.string.length; break; } case CX_JSON_NUMBER: { int precision = settings->frac_max_digits; // 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); char *dot, *exp; unsigned char max_digits; // find the decimal separator and hope that it's one of . or , dot = strchr(numbuf, '.'); if (dot == NULL) { dot = strchr(numbuf, ','); } if (dot == NULL) { // no decimal separator found // output everything until a possible exponent max_digits = 30; dot = numbuf; } else { // found a decimal separator // output everything until the separator // and set max digits to what the settings say size_t len = dot - numbuf; actual += wfunc(numbuf, 1, len, target); expected += len; max_digits = settings->frac_max_digits; if (max_digits > 15) { max_digits = 15; } // locale independent separator if (max_digits > 0) { actual += wfunc(".", 1, 1, target); expected++; } dot++; } // find the exponent exp = strchr(dot, 'e'); if (exp == NULL) { // no exponent - output the rest if (max_digits > 0) { size_t len = strlen(dot); if (len > max_digits) { len = max_digits; } actual += wfunc(dot, 1, len, target); expected += len; } } else { // exponent found - truncate the frac digits // and then output the rest if (max_digits > 0) { size_t len = exp - dot - 1; if (len > max_digits) { len = max_digits; } actual += wfunc(dot, 1, len, target); expected += len; } actual += wfunc("e", 1, 1, target); expected++; exp++; size_t len = strlen(exp); actual += wfunc(exp, 1, len, target); expected += len; } break; } case CX_JSON_INTEGER: { snprintf(numbuf, 32, "%" PRIi64, value->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) { actual += wfunc("true", 1, 4, target); expected += 4; } else if (value->value.literal == CX_JSON_FALSE) { actual += wfunc("false", 1, 5, target); expected += 5; } else { actual += wfunc("null", 1, 4, target); expected += 4; } break; } case CX_JSON_NOTHING: { // deliberately supported as an empty string! // users might want to just write the result // of a get operation without testing the value // and therefore this should not blow up break; } default: assert(false); // LCOV_EXCL_LINE } return expected != actual; } int cxJsonWrite( void *target, const CxJsonValue *value, cx_write_func wfunc, const CxJsonWriter *settings ) { assert(target != NULL); assert(value != NULL); assert(wfunc != NULL); CxJsonWriter writer_default = cxJsonWriterCompact(); if (settings == NULL) { settings = &writer_default; } return cx_json_write_rec(target, value, wfunc, settings, 0); }