rtcore/rtjson.h

559 lines
15 KiB
C

#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 <stdio.h>
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;
at += len;
*_at = at;
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;
at += len;
*_at = at;
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