// 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 #include #include #include #include #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; }