476 lines
14 KiB
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;
|
|
}
|