joeylib2/tools/common/toolJson.c

476 lines
14 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// toolJson.c - a small JSON reader for the JoeyLib host tools
// (toolJson.h).
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "toolBuf.h"
#include "toolFile.h"
#include "toolJson.h"
#define ASCII_END 0x80u
#define UTF8_2_MAX 0x7FFu
#define UTF8_3_MAX 0xFFFFu
#define UTF8_CONT 0x80u
#define UTF8_CONT_MASK 0x3Fu
#define UTF8_LEAD_2 0xC0u
#define UTF8_LEAD_3 0xE0u
#define UTF8_LEAD_4 0xF0u
#define UTF8_SHIFT 6u
#define SURROGATE_HI 0xD800u
#define SURROGATE_LO 0xDC00u
#define SURROGATE_END 0xE000u
#define SURROGATE_BITS 10u
#define SURROGATE_BASE 0x10000u
#define HEX_DIGITS 4u
#define HEX 16u
#define HEX_LETTER 10u
#define CONTROL_END 0x20u
// Arrays and objects nest this deep at most (Python's own limit is about
// as deep; it keeps a hostile file from overrunning the stack).
#define DEPTH_MAX 1000u
typedef struct ParserT {
const char *text;
size_t len;
size_t pos;
char *err;
bool failed;
} ParserT;
static void error(ParserT *p, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
static bool hex4(ParserT *p, uint32_t *code);
static void itemAdd(ToolJsonT *value, char *key, ToolJsonT *item);
static bool literal(ParserT *p, const char *word);
static ToolJsonT *newValue(ToolJsonTypeE type);
static bool number(ParserT *p, ToolJsonT *value);
static void skipSpace(ParserT *p);
static char *string(ParserT *p);
static void utf8Put(ToolBufT *out, uint32_t code);
static ToolJsonT *value(ParserT *p, uint32_t depth);
// The error, at its byte offset, unless one is already recorded.
static void error(ParserT *p, const char *fmt, ...) {
va_list args;
int n;
if (p->failed) {
return;
}
p->failed = true;
n = snprintf(p->err, TOOL_JSON_ERROR_BYTES, "char %zu: ", p->pos);
if (n < 0 || (size_t)n >= TOOL_JSON_ERROR_BYTES) {
return;
}
va_start(args, fmt);
vsnprintf(p->err + n, TOOL_JSON_ERROR_BYTES - (size_t)n, fmt, args);
va_end(args);
}
// Four hex digits at p->pos.
static bool hex4(ParserT *p, uint32_t *code) {
uint32_t i;
char c;
*code = 0u;
for (i = 0; i < HEX_DIGITS; i++) {
if (p->pos >= p->len) {
return false;
}
c = p->text[p->pos++];
if (c >= '0' && c <= '9') {
*code = *code * HEX + (uint32_t)(c - '0');
} else if (c >= 'a' && c <= 'f') {
*code = *code * HEX + (uint32_t)(c - 'a') + HEX_LETTER;
} else if (c >= 'A' && c <= 'F') {
*code = *code * HEX + (uint32_t)(c - 'A') + HEX_LETTER;
} else {
return false;
}
}
return true;
}
static void itemAdd(ToolJsonT *value, char *key, ToolJsonT *item) {
ToolJsonT **items;
char **keys;
items = (ToolJsonT **)realloc(value->items, (value->count + 1u) * sizeof(ToolJsonT *));
if (items == NULL) {
toolFail("out of memory");
}
value->items = items;
if (value->type == TOOL_JSON_OBJECT) {
keys = (char **)realloc(value->keys, (value->count + 1u) * sizeof(char *));
if (keys == NULL) {
toolFail("out of memory");
}
value->keys = keys;
value->keys[value->count] = key;
}
value->items[value->count] = item;
value->count++;
}
static bool literal(ParserT *p, const char *word) {
size_t n;
n = strlen(word);
if (p->pos + n <= p->len && memcmp(p->text + p->pos, word, n) == 0) {
p->pos += n;
return true;
}
return false;
}
static ToolJsonT *newValue(ToolJsonTypeE type) {
ToolJsonT *out;
out = (ToolJsonT *)calloc(1u, sizeof(ToolJsonT));
if (out == NULL) {
toolFail("out of memory");
}
out->type = type;
return out;
}
// A number at p->pos: -? digits (. digits)? ([eE] [+-]? digits)?.
static bool number(ParserT *p, ToolJsonT *value) {
size_t start;
char *copy;
char *end;
start = p->pos;
value->integer = true;
if (p->pos < p->len && p->text[p->pos] == '-') {
p->pos++;
}
if (p->pos >= p->len || p->text[p->pos] < '0' || p->text[p->pos] > '9') {
return false;
}
if (p->text[p->pos] == '0') {
p->pos++;
} else {
while (p->pos < p->len && p->text[p->pos] >= '0' && p->text[p->pos] <= '9') {
p->pos++;
}
}
if (p->pos < p->len && p->text[p->pos] == '.') {
value->integer = false;
p->pos++;
if (p->pos >= p->len || p->text[p->pos] < '0' || p->text[p->pos] > '9') {
return false;
}
while (p->pos < p->len && p->text[p->pos] >= '0' && p->text[p->pos] <= '9') {
p->pos++;
}
}
if (p->pos < p->len && (p->text[p->pos] == 'e' || p->text[p->pos] == 'E')) {
value->integer = false;
p->pos++;
if (p->pos < p->len && (p->text[p->pos] == '+' || p->text[p->pos] == '-')) {
p->pos++;
}
if (p->pos >= p->len || p->text[p->pos] < '0' || p->text[p->pos] > '9') {
return false;
}
while (p->pos < p->len && p->text[p->pos] >= '0' && p->text[p->pos] <= '9') {
p->pos++;
}
}
copy = (char *)malloc(p->pos - start + 1u);
if (copy == NULL) {
toolFail("out of memory");
}
memcpy(copy, p->text + start, p->pos - start);
copy[p->pos - start] = '\0';
value->number = strtod(copy, &end);
free(copy);
// An integer has no negative zero (Python reads -0 as the int 0).
if (value->integer && value->number == 0.0) {
value->number = 0.0;
}
return true;
}
static void skipSpace(ParserT *p) {
while (p->pos < p->len && (p->text[p->pos] == ' ' || p->text[p->pos] == '\t' || p->text[p->pos] == '\n' || p->text[p->pos] == '\r')) {
p->pos++;
}
}
// A string at p->pos (its opening quote), decoded and malloc'd; NULL (and
// an error) when it is not a good one.
static char *string(ParserT *p) {
ToolBufT out;
uint32_t code;
uint32_t low;
char c;
memset(&out, 0, sizeof(out));
p->pos++;
for (;;) {
if (p->pos >= p->len) {
error(p, "unterminated string");
toolBufFree(&out);
return NULL;
}
c = p->text[p->pos++];
if (c == '"') {
break;
}
if ((unsigned char)c < CONTROL_END) {
error(p, "control character in a string");
toolBufFree(&out);
return NULL;
}
if (c != '\\') {
toolBufByte(&out, (uint8_t)c);
continue;
}
if (p->pos >= p->len) {
error(p, "unterminated string");
toolBufFree(&out);
return NULL;
}
c = p->text[p->pos++];
switch (c) {
case '"':
case '\\':
case '/':
toolBufByte(&out, (uint8_t)c);
break;
case 'b':
toolBufByte(&out, '\b');
break;
case 'f':
toolBufByte(&out, '\f');
break;
case 'n':
toolBufByte(&out, '\n');
break;
case 'r':
toolBufByte(&out, '\r');
break;
case 't':
toolBufByte(&out, '\t');
break;
case 'u':
if (!hex4(p, &code)) {
error(p, "bad \\u escape");
toolBufFree(&out);
return NULL;
}
// A high surrogate followed by a low one is one character.
if (code >= SURROGATE_HI && code < SURROGATE_LO && p->pos + 1u < p->len && p->text[p->pos] == '\\' && p->text[p->pos + 1u] == 'u') {
p->pos += 2u;
if (!hex4(p, &low)) {
error(p, "bad \\u escape");
toolBufFree(&out);
return NULL;
}
if (low >= SURROGATE_LO && low < SURROGATE_END) {
code = SURROGATE_BASE + ((code - SURROGATE_HI) << SURROGATE_BITS) + (low - SURROGATE_LO);
} else {
utf8Put(&out, code);
code = low;
}
}
utf8Put(&out, code);
break;
default:
error(p, "bad escape '\\%c'", c);
toolBufFree(&out);
return NULL;
}
}
toolBufByte(&out, '\0');
return (char *)out.data;
}
void toolJsonFree(ToolJsonT *v) {
size_t i;
if (v == NULL) {
return;
}
for (i = 0; i < v->count; i++) {
toolJsonFree(v->items[i]);
if (v->keys != NULL) {
free(v->keys[i]);
}
}
free(v->items);
free(v->keys);
free(v->string);
free(v);
}
const ToolJsonT *toolJsonGet(const ToolJsonT *v, const char *key) {
size_t i;
if (v == NULL || v->type != TOOL_JSON_OBJECT) {
return NULL;
}
for (i = v->count; i-- > 0u;) {
if (strcmp(v->keys[i], key) == 0) {
return v->items[i];
}
}
return NULL;
}
ToolJsonT *toolJsonParse(const char *text, size_t len, char err[TOOL_JSON_ERROR_BYTES]) {
ParserT p;
ToolJsonT *root;
p.text = text;
p.len = len;
p.pos = 0u;
p.err = err;
p.failed = false;
err[0] = '\0';
root = value(&p, 0u);
if (root != NULL) {
skipSpace(&p);
if (p.pos < p.len) {
error(&p, "extra data after the value");
toolJsonFree(root);
root = NULL;
}
}
return root;
}
// A code point as UTF-8 (a lone surrogate as its own three bytes).
static void utf8Put(ToolBufT *out, uint32_t code) {
if (code < ASCII_END) {
toolBufByte(out, code);
} else if (code <= UTF8_2_MAX) {
toolBufByte(out, UTF8_LEAD_2 | (code >> UTF8_SHIFT));
toolBufByte(out, UTF8_CONT | (code & UTF8_CONT_MASK));
} else if (code <= UTF8_3_MAX) {
toolBufByte(out, UTF8_LEAD_3 | (code >> (2u * UTF8_SHIFT)));
toolBufByte(out, UTF8_CONT | ((code >> UTF8_SHIFT) & UTF8_CONT_MASK));
toolBufByte(out, UTF8_CONT | (code & UTF8_CONT_MASK));
} else {
toolBufByte(out, UTF8_LEAD_4 | (code >> (3u * UTF8_SHIFT)));
toolBufByte(out, UTF8_CONT | ((code >> (2u * UTF8_SHIFT)) & UTF8_CONT_MASK));
toolBufByte(out, UTF8_CONT | ((code >> UTF8_SHIFT) & UTF8_CONT_MASK));
toolBufByte(out, UTF8_CONT | (code & UTF8_CONT_MASK));
}
}
// A value at p->pos (after any space), or NULL with an error.
static ToolJsonT *value(ParserT *p, uint32_t depth) {
ToolJsonT *out;
ToolJsonT *item;
char *key;
char close;
skipSpace(p);
if (depth > DEPTH_MAX) {
error(p, "nested too deeply");
return NULL;
}
if (p->pos >= p->len) {
error(p, "a value was expected");
return NULL;
}
if (p->text[p->pos] == '{' || p->text[p->pos] == '[') {
out = newValue(p->text[p->pos] == '{' ? TOOL_JSON_OBJECT : TOOL_JSON_ARRAY);
close = p->text[p->pos] == '{' ? '}' : ']';
p->pos++;
skipSpace(p);
if (p->pos < p->len && p->text[p->pos] == close) {
p->pos++;
return out;
}
for (;;) {
key = NULL;
if (out->type == TOOL_JSON_OBJECT) {
skipSpace(p);
if (p->pos >= p->len || p->text[p->pos] != '"') {
error(p, "a key was expected");
toolJsonFree(out);
return NULL;
}
key = string(p);
skipSpace(p);
if (key == NULL || p->pos >= p->len || p->text[p->pos] != ':') {
error(p, "':' was expected");
free(key);
toolJsonFree(out);
return NULL;
}
p->pos++;
}
item = value(p, depth + 1u);
if (item == NULL) {
free(key);
toolJsonFree(out);
return NULL;
}
itemAdd(out, key, item);
skipSpace(p);
if (p->pos < p->len && p->text[p->pos] == ',') {
p->pos++;
continue;
}
if (p->pos < p->len && p->text[p->pos] == close) {
p->pos++;
return out;
}
error(p, "',' or '%c' was expected", close);
toolJsonFree(out);
return NULL;
}
}
if (p->text[p->pos] == '"') {
out = newValue(TOOL_JSON_STRING);
out->string = string(p);
if (out->string == NULL) {
toolJsonFree(out);
return NULL;
}
return out;
}
if (literal(p, "true")) {
out = newValue(TOOL_JSON_BOOL);
out->boolean = true;
return out;
}
if (literal(p, "false")) {
return newValue(TOOL_JSON_BOOL);
}
if (literal(p, "null")) {
return newValue(TOOL_JSON_NULL);
}
out = newValue(TOOL_JSON_NUMBER);
if (!number(p, out)) {
error(p, "a value was expected");
toolJsonFree(out);
return NULL;
}
return out;
}