From e244af9c383c98df3de23f2b49a78bbcbea6001c Mon Sep 17 00:00:00 2001 From: Kevin Trogant Date: Fri, 9 Jan 2026 19:39:09 +0100 Subject: [PATCH] first version of rtjson.h --- rtjson.h | 554 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 554 insertions(+) create mode 100644 rtjson.h diff --git a/rtjson.h b/rtjson.h new file mode 100644 index 0000000..364c8ab --- /dev/null +++ b/rtjson.h @@ -0,0 +1,554 @@ +#ifndef RT_JSON_H +#define RT_JSON_H + +/* + * rtjson.h - Simple json parser for C + * Copyright (C) 2026 Kevin Trogant + * + * 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. + */ + +#include "rtcore.h" + +#ifndef RTJ_API + #define RTJ_API RTC_API +#endif + +typedef enum +{ + JSON_TYPE_OBJECT, + JSON_TYPE_ARRAY, + JSON_TYPE_STRING, + JSON_TYPE_INTEGER, + JSON_TYPE_FLOAT, +} json_type; + +/* Represents a json value */ +typedef struct json +{ + /* The key, if this value is part of an object. + * Empty otherwise. */ + s8 key; + + json_type type; + + /* The value. */ + union + { + s8 s; + i64 i; + f64 f; + + /* The first subobject. + * Element 0 of an array, + * one member of an object. */ + struct json *first_child; + } value; + + /* Next in the list of a parents children */ + struct json *next; +} json; + +/* Parses the given json text. + * The returned json struct will be allocated on a. + * file is only used for error output and can be set to {0} */ +RTJ_API json *ParseJSON(s8 text, s8 file, arena *a); + +/* Returns a pointer to the objects member with the given key. + * Returns NULL, if no such member exists */ +RTJ_API json *GetJSONMember(json *o, s8 key); + +/* Returns a pointer to the arrays i-th element. + * Returns NULL if no such element exists */ +RTJ_API json *GetJSONArrayElement(json *a, isize i); + +/* Returns the length of the given json array or -1 if the + * passed pointer is not an array */ +RTJ_API isize GetJSONArrayLength(json *a); + +/* Iterates over every member of an object or array */ +#define JsonForEach(_ChildVarName, _ParentPtr) \ + for (json *_ChildVarName = (_ParentPtr)->value.first_child; _ChildVarName != 0; _ChildVarName = _ChildVarName->next) + +#endif + +#ifdef RT_JSON_IMPLEMENTATION +#undef RT_JSON_IMPLEMENTATION + +#include + +typedef enum +{ + NOT_A_NUMBER, + DOUBLE, + INT, +} parse_number_result; + +static parse_number_result +ParseNumber(s8 text, isize *_at, isize *current_line, s8 file, i64 *_i, f64 *_f) +{ + isize at = *_at; + if ((text.data[at] < '0' || text.data[at] > '9') && text.data[at] != '-') + return NOT_A_NUMBER; + int dot_count = 0, exp_count = 0; + isize len = 0; + while ((text.data[at + len] >= '0' && text.data[at + len] <= '9') || text.data[at + len] == '.' || + text.data[at + len] == 'e' || text.data[at + len] == 'E' || text.data[at + len] == '+' || + text.data[at + len] == '-') + { + ++len; + if (text.data[at + len] == '.') + ++dot_count; + if (text.data[at + len] == 'e' || text.data[at + len] == 'E') + ++exp_count; + } + if (dot_count > 1 || exp_count > 1) + { + if (file.data) + printf("%*.s:%zu expected a number\n", (int)file.length, file.data, *current_line); + else + printf("%zu: expected a number\n", *current_line); + return NOT_A_NUMBER; + } + if (dot_count || exp_count) + { + s8 n = {.data = &text.data[at], .length = len}; + s8_parse_f64_result parsed = S8ParseF64(n); + *_f = parsed.f; + return parsed.ok ? DOUBLE : NOT_A_NUMBER; + } + else + { + s8 n = {.data = &text.data[at], .length = len}; + s8_parse_i64_result parsed = S8ParseI64(n, 10); + *_i = parsed.i; + return parsed.ok ? INT : NOT_A_NUMBER; + } +} + +static s8 +ParseString(s8 text, isize *_at, isize *current_line, s8 file, arena *a) +{ + isize at = *_at; + if (text.data[at] != '\"') + { + if (file.data) + printf("%.*s:%zu expected string\n", (int)file.length, file.data, *current_line); + else + printf("%zu: expected string\n", *current_line); + return (s8){0}; + } + ++at; + isize first_char = at; + s8 str = {.data = &text.data[first_char], .length = 0}; + /* If the string contains control codes, we need to copy it into owned + * memory to be able to modify it. + */ + b32 owns_memory = 0; + isize str_at = 0; + while (text.data[at] != '\"' && at < text.length) + { + if (text.data[at] == '\\') + { + /* Control codes. + * If this string is still unowned, copy it into owned memory to be able + * to modify it. */ + if (!owns_memory) + { + isize total_length = at - first_char; + isize i = at; + while (text.data[i] != '\"' && i < text.length) + ++total_length; + if (i == text.length) + { + if (file.data) + printf("%.*s:%zu unexpected end of file\n", (int)file.length, file.data, *current_line); + else + printf("%zu: unexpected end of file\n", *current_line); + return (s8){0}; + } + u8 *mem = alloc(a, u8, total_length); + if (at > first_char) + memcpy(mem, str.data, at - first_char); + str.data = mem; + owns_memory = 1; + } + + ++at; + if (at == text.length) + { + if (file.data) + printf("%.*s:%zu unexpected end of file\n", (int)file.length, file.data, *current_line); + else + printf("%zu: unexpected end of file\n", *current_line); + return (s8){0}; + } + switch (text.data[at]) + { + case '\"': + case '\\': + str.data[str_at] = text.data[at]; + ++str_at; + ++at; + break; + case 'b': + str.data[str_at] = '\b'; + ++str_at; + ++at; + break; + case 'f': + str.data[str_at] = '\f'; + ++str_at; + ++at; + break; + case 'n': + str.data[str_at] = '\n'; + ++str_at; + ++at; + break; + case 'r': + str.data[str_at] = '\r'; + ++str_at; + ++at; + break; + case 't': + str.data[str_at] = '\t'; + ++str_at; + ++at; + break; + case 'u': + /* 4 hex digits unicode codepoint + * NOT IMPLEMENTED YET. */ + default: + if (file.data) + printf("%.*s:%zu unsupported control code '%c'\n", + (int)file.length, + file.data, + *current_line, + text.data[at]); + else + printf("%zu: unsupported control code '%c'\n", *current_line, text.data[at]); + return (s8){0}; + } + } + else + { + /* Normal character */ + if (owns_memory) + str.data[str_at] = text.data[at]; + ++str_at; + ++at; + } + } + /* Consume terminating quotes */ + ++at; + str.length = str_at; + + *_at = at; + return str; +} + +/* Return 1 if it reaches the ned of the file */ +static b32 +ConsumeWhitespace(s8 text, isize *_at, isize *current_line) +{ + isize at = *_at; + while (at < text.length) + { + if (text.data[at] == ' ' || text.data[at] == '\t' || text.data[at] == '\r') + { + ++at; + } + else if (text.data[at] == '\n') + { + ++at; + ++*current_line; + } + else + { + break; + } + } + *_at = at; + return at == text.length; +} + +#define ConsumeWhitespaceNoEof(_Text, _PtrAt, _CurLine, _File, _ErrRet) \ + if (ConsumeWhitespace((_Text), (_PtrAt), (_CurLine))) \ + { \ + if ((_File).data) \ + printf("%.*s:%zu unexpected end of file\n", (int)(_File).length, (_File).data, *(_CurLine)); \ + else \ + printf("%zu: unexpected end of file\n", *(_CurLine)); \ + return (_ErrRet); \ + } + +#define ExpectCharacter(_Char, _Text, _At, _CurLine, _File, _ErrRet) \ + if ((_Text).data[(_At)] != (_Char)) \ + { \ + if ((_File).data) \ + printf("%.*s:%zu expected '%c', got '%c'\n", \ + (int)(_File).length, \ + (_File).data, \ + *(_CurLine), \ + (_Char), \ + (_Text).data[(_At)]); \ + else \ + printf("%zu: expected '%c' got '%c'\n", *(_CurLine), (_Char), (_Text).data[(_At)]); \ + return (_ErrRet); \ + } \ + (_At) += 1; + +static json * +ParseJSONImpl(s8 text, isize *_at, json *sibling, isize *current_line, s8 file, arena *a) +{ + isize at = *_at; + + json *j = alloc(a, json); + j->next = sibling; + + if (text.data[at] == '{') + { + /* Parse object */ + ++at; + + j->type = JSON_TYPE_OBJECT; + + ConsumeWhitespaceNoEof(text, &at, current_line, file, 0); + + while (text.data[at] != '}' && at < text.length) + { + /* "string" : value [,] */ + s8 key = ParseString(text, &at, current_line, file, a); + if (!key.data) + return 0; + + ConsumeWhitespaceNoEof(text, &at, current_line, file, 0); + ExpectCharacter(':', text, at, current_line, file, 0); + ConsumeWhitespaceNoEof(text, &at, current_line, file, 0); + + /* Value */ + if (text.data[at] == '{' || text.data[at] == '[') + { + /* Object or array, recurse into it. + * JSON objects are unordered, so it does not matter that we + * effectively reverse the order here */ + j->value.first_child = ParseJSONImpl(text, &at, j->value.first_child, current_line, file, a); + if (!j->value.first_child) + return 0; + j->value.first_child->key = key; + } + else if (text.data[at] == '\"') + { + json *child = alloc(a, json); + child->key = key; + child->type = JSON_TYPE_STRING; + child->value.s = ParseString(text, &at, current_line, file, a); + child->next = j->value.first_child; + j->value.first_child = child; + } + else if ((text.data[at] >= '0' && text.data[at] <= '9') || text.data[at] == '-') + { + /* Number. */ + f64 f; + i64 i; + parse_number_result res = ParseNumber(text, &at, current_line, file, &i, &f); + if (res == NOT_A_NUMBER) + return 0; + json *child = alloc(a, json); + child->key = key; + if (res == INT) + { + child->type = JSON_TYPE_INTEGER; + child->value.i = i; + } + else /* DOUBLE */ + { + child->type = JSON_TYPE_FLOAT; + child->value.f = f; + } + child->next = j->value.first_child; + j->value.first_child = child; + } + + ConsumeWhitespaceNoEof(text, &at, current_line, file, 0); + + /* Consume ',' We deviate from the spec: We actually support trailing commas, + * because a) thats simpler code and b) not allowing trailing commas is stupid. */ + if (text.data[at] == ',') + { + ++at; + ConsumeWhitespaceNoEof(text, &at, current_line, file, 0); + } + else if (text.data[at] != '}') + { + if (file.data) + printf("%.*s:%zu unexpected character '%c' after value\n", + (int)file.length, + file.data, + *current_line, + text.data[at]); + else + printf("%zu: unexpected character '%c' after value\n", *current_line, text.data[at]); + return 0; + } + } + ++at; + } + else if (text.data[at] == '[') + { + /* Parse array */ + ++at; + j->type = JSON_TYPE_ARRAY; + + json *last_child = NULL; + while (text.data[at] != ']' && at < text.length) + { + /* value [,] ... */ + ConsumeWhitespaceNoEof(text, &at, current_line, file, 0); + if (text.data[at] == '{' || text.data[at] == '[') + { + /* Object or array, recurse into it. */ + json *child = ParseJSONImpl(text, &at, NULL, current_line, file, a); + if (!child) + return 0; + if (!j->value.first_child) + j->value.first_child = child; + if (last_child) + last_child->next = child; + last_child = child; + } + else if (text.data[at] == '\"') + { + json *child = alloc(a, json); + child->value.s = ParseString(text, &at, current_line, file, a); + child->type = JSON_TYPE_STRING; + if (!j->value.first_child) + j->value.first_child = child; + if (last_child) + last_child->next = child; + last_child = child; + } + else if ((text.data[at] >= '0' && text.data[at] <= '9') || text.data[at] == '-') + { + /* Number. */ + f64 f; + i64 i; + parse_number_result res = ParseNumber(text, &at, current_line, file, &i, &f); + if (res == NOT_A_NUMBER) + return 0; + json *child = alloc(a, json); + if (res == INT) + { + child->value.i = i; + child->type = JSON_TYPE_INTEGER; + } + else /* DOUBLE */ + { + child->value.f = f; + child->type = JSON_TYPE_FLOAT; + } + if (!j->value.first_child) + j->value.first_child = child; + if (last_child) + last_child->next = child; + last_child = child; + } + + ConsumeWhitespaceNoEof(text, &at, current_line, file, 0); + + /* Consume ',' We deviate from the spec: We actually support trailing commas, + * because a) thats simpler code and b) not allowing trailing commas is stupid. */ + if (text.data[at] == ',') + { + ++at; + ConsumeWhitespaceNoEof(text, &at, current_line, file, 0); + } + else if (text.data[at] != ']') + { + if (file.data) + printf("%.*s:%zu unexpected character '%c' after value\n", + (int)file.length, + file.data, + *current_line, + text.data[at]); + else + printf("%zu: unexpected character '%c' after value\n", *current_line, text.data[at]); + return 0; + } + } + ++at; + } + else + { + if (file.data) + printf("%.*s:%zu unexpected character %c\n", (int)file.length, file.data, *current_line, text.data[0]); + else + printf("%zu: unexpected character %c\n", *current_line, text.data[0]); + } + *_at = at; + return j; +} + +RTJ_API json * +ParseJSON(s8 text, s8 file, arena *a) +{ + isize line = 1, at = 0; + return ParseJSONImpl(text, &at, NULL, &line, file, a); +} + +RTJ_API json * +GetJSONMember(json *o, s8 key) +{ + if (o->type != JSON_TYPE_OBJECT) + return NULL; + JsonForEach(child, o) + { + if (S8Equals(child->key, key)) + return child; + } + return NULL; +} + +RTJ_API json * +GetJSONArrayElement(json *a, isize i) +{ + if (a->type != JSON_TYPE_ARRAY) + return NULL; + isize at = 0; + JsonForEach(child, a) + { + if (at == i) + return child; + ++at; + } + return NULL; +} + +RTJ_API isize +GetJSONArrayLength(json *a) +{ + if (a->type != JSON_TYPE_ARRAY) + return -1; + isize at = 0; + JsonForEach(child, a) { ++at; } + return at; +} + +#endif