diff options
| -rw-r--r-- | kon/konjson.h | 494 |
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 |
