/* SPDX-License-Identifier: MIT */ /* * konjson.h * * Copyright (c) 2026 KonAki * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #ifndef KONJSON_H #define KONJSON_H #include #ifdef __cplusplus extern "C" { #endif typedef enum kon_jsonType { KON_JSON_NULL, KON_JSON_BOOL, KON_JSON_NUMBER, KON_JSON_STRING, KON_JSON_ARRAY, KON_JSON_OBJECT } kon_jsonType_t; /* children of an array or object sit in items, an object's keys sit in the same slots as the values */ typedef struct kon_json { kon_jsonType_t type; double number; char *string; char *key; struct kon_json *items; int count; } kon_json_t; /* error gets a short message with the line number when parsing fails, it may be NULL */ kon_json_t *kon_jsonParse(const char *text, size_t length, char *error, int errorSize); kon_json_t *kon_jsonParseFile(const char *path, char *error, int errorSize); void kon_jsonFree(kon_json_t *json); /* all of these accept NULL and give the default (or NULL) when the type is wrong, so lookups can be chained */ const kon_json_t *kon_jsonGet(const kon_json_t *object, const char *key); const kon_json_t *kon_jsonAt(const kon_json_t *array, int index); int kon_jsonCount(const kon_json_t *json); const char *kon_jsonString(const kon_json_t *json, const char *def); double kon_jsonNumber(const kon_json_t *json, double def); int kon_jsonInt(const kon_json_t *json, int def); int kon_jsonBool(const kon_json_t *json, int def); #ifdef __cplusplus } #endif /*** implementation ***/ #ifdef KONJSON_IMPLEMENTATION #include #include #include #define KON_JSON_DEPTH 200 typedef struct kon_jsonParser { const char *p; const char *end; const char *begin; char *error; int errorSize; int failed; } kon_jsonParser_t; static void kon__jsonClear(kon_json_t *json) { int i; for (i = 0; i < json->count; i++) { kon__jsonClear(&json->items[i]); free(json->items[i].key); } free(json->items); free(json->string); json->items = NULL; json->string = NULL; json->count = 0; } static int kon__jsonFail(kon_jsonParser_t *ps, const char *message) { const char *c; int line = 1; if (!ps->failed && ps->error && ps->errorSize > 0) { for (c = ps->begin; c < ps->p && c < ps->end; c++) { if (*c == '\n') line++; } snprintf(ps->error, (size_t)ps->errorSize, "line %d: %s", line, message); } ps->failed = 1; return 0; } static void kon__jsonSpace(kon_jsonParser_t *ps) { while (ps->p < ps->end && (*ps->p == ' ' || *ps->p == '\t' || *ps->p == '\n' || *ps->p == '\r')) ps->p++; } static int kon__jsonHex4(const char *p, unsigned *out) { unsigned v = 0; int i; for (i = 0; i < 4; i++) { char c = p[i]; v <<= 4; if (c >= '0' && c <= '9') v |= (unsigned)(c - '0'); else if (c >= 'a' && c <= 'f') v |= (unsigned)(c - 'a' + 10); else if (c >= 'A' && c <= 'F') v |= (unsigned)(c - 'A' + 10); else return 0; } *out = v; return 1; } static int kon__jsonUtf8(char *out, unsigned cp) { if (cp < 0x80) { out[0] = (char)cp; return 1; } if (cp < 0x800) { out[0] = (char)(0xC0 | (cp >> 6)); out[1] = (char)(0x80 | (cp & 0x3F)); return 2; } if (cp < 0x10000) { out[0] = (char)(0xE0 | (cp >> 12)); out[1] = (char)(0x80 | ((cp >> 6) & 0x3F)); out[2] = (char)(0x80 | (cp & 0x3F)); return 3; } out[0] = (char)(0xF0 | (cp >> 18)); out[1] = (char)(0x80 | ((cp >> 12) & 0x3F)); out[2] = (char)(0x80 | ((cp >> 6) & 0x3F)); out[3] = (char)(0x80 | (cp & 0x3F)); return 4; } static char *kon__jsonString(kon_jsonParser_t *ps) { const char *start, *scan; char *out; size_t n = 0; ps->p++; start = ps->p; for (scan = start; scan < ps->end && *scan != '"'; scan++) { if (*scan == '\\' && scan + 1 < ps->end) scan++; } out = malloc((size_t)(scan - start) + 1); if (!out) { kon__jsonFail(ps, "out of memory"); return NULL; } while (ps->p < ps->end && *ps->p != '"') { unsigned char c = (unsigned char)*ps->p; if (c < 0x20) { kon__jsonFail(ps, "control character in string"); free(out); return NULL; } if (c != '\\') { out[n++] = (char)c; ps->p++; continue; } ps->p++; if (ps->p >= ps->end) break; switch (*ps->p) { case '"': out[n++] = '"'; break; case '\\': out[n++] = '\\'; break; case '/': out[n++] = '/'; break; case 'b': out[n++] = '\b'; break; case 'f': out[n++] = '\f'; break; case 'n': out[n++] = '\n'; break; case 'r': out[n++] = '\r'; break; case 't': out[n++] = '\t'; break; case 'u': { unsigned cp, lo; if (ps->end - ps->p < 5 || !kon__jsonHex4(ps->p + 1, &cp)) { kon__jsonFail(ps, "bad \\u escape"); free(out); return NULL; } ps->p += 4; if (cp >= 0xD800 && cp < 0xDC00 && ps->end - ps->p >= 7 && ps->p[1] == '\\' && ps->p[2] == 'u' && kon__jsonHex4(ps->p + 3, &lo) && lo >= 0xDC00 && lo < 0xE000) { cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00); ps->p += 6; } else if (cp >= 0xD800 && cp < 0xE000) { cp = 0xFFFD; } n += (size_t)kon__jsonUtf8(out + n, cp); break; } default: kon__jsonFail(ps, "bad escape"); free(out); return NULL; } ps->p++; } if (ps->p >= ps->end) { kon__jsonFail(ps, "unterminated string"); free(out); return NULL; } ps->p++; out[n] = '\0'; return out; } static int kon__jsonValue(kon_jsonParser_t *ps, kon_json_t *out, int depth); static int kon__jsonPush(kon_jsonParser_t *ps, kon_json_t *parent, int *capacity, const kon_json_t *item) { if (parent->count == *capacity) { int grown = *capacity ? *capacity * 2 : 8; kon_json_t *items = realloc(parent->items, (size_t)grown * sizeof(kon_json_t)); if (!items) return kon__jsonFail(ps, "out of memory"); parent->items = items; *capacity = grown; } parent->items[parent->count++] = *item; return 1; } static int kon__jsonContainer(kon_jsonParser_t *ps, kon_json_t *out, int depth) { int isObject = *ps->p == '{'; char close = isObject ? '}' : ']'; int capacity = 0; out->type = isObject ? KON_JSON_OBJECT : KON_JSON_ARRAY; ps->p++; kon__jsonSpace(ps); if (ps->p < ps->end && *ps->p == close) { ps->p++; return 1; } for (;;) { kon_json_t item; char *key = NULL; memset(&item, 0, sizeof item); kon__jsonSpace(ps); if (isObject) { if (ps->p >= ps->end || *ps->p != '"') return kon__jsonFail(ps, "expected a key"); key = kon__jsonString(ps); if (!key) return 0; kon__jsonSpace(ps); if (ps->p >= ps->end || *ps->p != ':') { free(key); return kon__jsonFail(ps, "expected ':'"); } ps->p++; } if (!kon__jsonValue(ps, &item, depth + 1)) { kon__jsonClear(&item); free(key); return 0; } item.key = key; if (!kon__jsonPush(ps, out, &capacity, &item)) { kon__jsonClear(&item); free(key); return 0; } kon__jsonSpace(ps); if (ps->p < ps->end && *ps->p == ',') { ps->p++; continue; } if (ps->p < ps->end && *ps->p == close) { ps->p++; return 1; } return kon__jsonFail(ps, isObject ? "expected ',' or '}'" : "expected ',' or ']'"); } } static int kon__jsonValue(kon_jsonParser_t *ps, kon_json_t *out, int depth) { size_t left; char *end; if (depth > KON_JSON_DEPTH) return kon__jsonFail(ps, "nested too deep"); kon__jsonSpace(ps); if (ps->p >= ps->end) return kon__jsonFail(ps, "unexpected end"); left = (size_t)(ps->end - ps->p); switch (*ps->p) { case '{': case '[': return kon__jsonContainer(ps, out, depth); case '"': out->type = KON_JSON_STRING; out->string = kon__jsonString(ps); return out->string != NULL; case 't': if (left >= 4 && !memcmp(ps->p, "true", 4)) { out->type = KON_JSON_BOOL; out->number = 1; ps->p += 4; return 1; } break; case 'f': if (left >= 5 && !memcmp(ps->p, "false", 5)) { out->type = KON_JSON_BOOL; ps->p += 5; return 1; } break; case 'n': if (left >= 4 && !memcmp(ps->p, "null", 4)) { out->type = KON_JSON_NULL; ps->p += 4; return 1; } break; default: if (*ps->p == '-' || (*ps->p >= '0' && *ps->p <= '9')) { char buf[64]; size_t n = 0; while (n < left && n < sizeof buf - 1 && (strchr("+-.eE", ps->p[n]) || (ps->p[n] >= '0' && ps->p[n] <= '9'))) { buf[n] = ps->p[n]; n++; } buf[n] = '\0'; out->number = strtod(buf, &end); if (end == buf) return kon__jsonFail(ps, "bad number"); out->type = KON_JSON_NUMBER; ps->p += end - buf; return 1; } break; } return kon__jsonFail(ps, "unexpected character"); } void kon_jsonFree(kon_json_t *json) { if (!json) return; kon__jsonClear(json); free(json); } kon_json_t *kon_jsonParse(const char *text, size_t length, char *error, int errorSize) { kon_jsonParser_t ps; kon_json_t *root; if (error && errorSize > 0) error[0] = '\0'; if (length >= 3 && !memcmp(text, "\xEF\xBB\xBF", 3)) { text += 3; length -= 3; } ps.begin = ps.p = text; ps.end = text + length; ps.error = error; ps.errorSize = errorSize; ps.failed = 0; root = calloc(1, sizeof *root); if (!root) return NULL; if (!kon__jsonValue(&ps, root, 0)) { kon_jsonFree(root); return NULL; } kon__jsonSpace(&ps); if (ps.p != ps.end) { kon__jsonFail(&ps, "text after the value"); kon_jsonFree(root); return NULL; } return root; } kon_json_t *kon_jsonParseFile(const char *path, char *error, int errorSize) { FILE *f = fopen(path, "rb"); long size; char *text; size_t got; kon_json_t *json; if (error && errorSize > 0) error[0] = '\0'; if (!f) { if (error) snprintf(error, (size_t)errorSize, "cannot open %s", path); return NULL; } if (fseek(f, 0, SEEK_END) != 0 || (size = ftell(f)) < 0 || fseek(f, 0, SEEK_SET) != 0) { fclose(f); if (error) snprintf(error, (size_t)errorSize, "cannot size %s", path); return NULL; } text = malloc((size_t)size + 1); if (!text) { fclose(f); return NULL; } got = fread(text, 1, (size_t)size, f); fclose(f); json = kon_jsonParse(text, got, error, errorSize); free(text); return json; } const kon_json_t *kon_jsonGet(const kon_json_t *object, const char *key) { int i; if (!object || object->type != KON_JSON_OBJECT) return NULL; for (i = 0; i < object->count; i++) { if (object->items[i].key && !strcmp(object->items[i].key, key)) return &object->items[i]; } return NULL; } const kon_json_t *kon_jsonAt(const kon_json_t *array, int index) { if (!array || array->type != KON_JSON_ARRAY || index < 0 || index >= array->count) return NULL; return &array->items[index]; } int kon_jsonCount(const kon_json_t *json) { if (!json || (json->type != KON_JSON_ARRAY && json->type != KON_JSON_OBJECT)) return 0; return json->count; } const char *kon_jsonString(const kon_json_t *json, const char *def) { return json && json->type == KON_JSON_STRING ? json->string : def; } double kon_jsonNumber(const kon_json_t *json, double def) { return json && json->type == KON_JSON_NUMBER ? json->number : def; } int kon_jsonInt(const kon_json_t *json, int def) { double v; if (!json || json->type != KON_JSON_NUMBER) return def; v = json->number; if (v > 2147483647.0) return 2147483647; if (v < -2147483648.0) return -2147483647 - 1; return (int)v; } int kon_jsonBool(const kon_json_t *json, int def) { return json && json->type == KON_JSON_BOOL ? json->number != 0 : def; } #endif /* end of KONJSON_IMPLEMENTATION */ #endif