summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--kon/konjson.h494
1 files changed, 494 insertions, 0 deletions
diff --git a/kon/konjson.h b/kon/konjson.h
new file mode 100644
index 0000000..1e95990
--- /dev/null
+++ b/kon/konjson.h
@@ -0,0 +1,494 @@
+/* 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 <stddef.h>
+
+#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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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;
+ char *out;
+ size_t n = 0;
+
+ ps->p++;
+ start = ps->p;
+ out = malloc((size_t)(ps->end - 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