diff --git a/bin/json.c b/bin/json.c new file mode 100755 index 0000000..9c2da61 --- /dev/null +++ b/bin/json.c @@ -0,0 +1,1125 @@ +//usr/bin/env tcc -run -bt -g -b -DJSON_LIBRARY_MAIN "$0" "$@"; exit +/* + json.c - small single-file JSON parser, serializer, query helper, and CLI. + + This file is meant to work in three modes: + + 1. Command line utility + Run it directly, or compile with JSON_LIBRARY_MAIN: + + ./json.c --test + printf '{"name":["zero","one"]}' | ./json.c 'name[1]' + ./json.c 'chart.result[0].meta.symbol' acwi + + Without --test it reads JSON from stdin, or from an optional file argument, + applies an optional query, and pretty-prints the selected JSON node. + + usage: ./json.c [query] [file] + + Query syntax supports dotted object fields and bracket array indexes: + + name + items[0] + config.timeout_ms + chart.result[0].meta.symbol + + 2. Header-only declarations + Include this file normally to get the public types and function prototypes: + + #include "json.c" + + int main(void) { + json_t *root = json_read("data.json"); + char *name = jqs(root, "user.name"); + double price = jqn(root, "items[0].price"); + json_free(root); + } + + Link with one translation unit compiled with JSON_LIBRARY_IMPL. + + 3. Library implementation + In exactly one .c file, define JSON_LIBRARY_IMPL before including json.c, + or compile json.c directly with -DJSON_LIBRARY_IMPL: + + #define JSON_LIBRARY_IMPL + #include "json.c" + + gcc -DJSON_LIBRARY_IMPL -c json.c -o json.o + + Public API: + + json_t *json_parse(const char *stream) + Parse a NUL-terminated JSON string. Returns NULL on error. + + json_t *json_read(char *filename) + Read a file and parse it as JSON. Returns NULL on read/parse error. + + char *json_serialize_compact(json_t *n) + char *json_serialize_pretty(json_t *n, const char *new_line, const char *indent) + Serialize a JSON tree. Returned strings are heap allocated; free them. + + void json_print(json_t *n) + Pretty-print a JSON value to stdout. + + json_t *json_object_get(json_t *n, const char *key) + json_t *json_array_get(json_t *n, int idx) + Direct object/array access helpers. Return NULL on wrong type/miss. + + json_t *jq(json_t *n, const char *fmt, ...) + double jqn(json_t *n, const char *fmt, ...) + char *jqs(json_t *n, const char *fmt, ...) + int jql(json_t *n, const char *fmt, ...) + Query helpers. jq returns a node, jqn extracts a number, jqs extracts + a string, and jql returns array/object length. Queries use dotted + object fields and bracket array indexes. The query format is + printf-style, so indexes can be formatted: jqs(root, "items[%d]", i). + + int json_equal(json_t *a, json_t *b) + Structural equality test. + + void json_free(json_t *n) + Recursively free a JSON tree returned by this library. + + Data model: + Inspect n->kind, then use n->number, n->string, n->boolean, n->array, or + n->object. Arrays contain json_t* elements. Objects contain key/value pairs. + + Notes: + - Supports null, booleans, numbers, strings, arrays, and objects. + - Stops at the first parse error and prints an error with line/column. + - Object member order is preserved. + + Refactors: + - Parsing error handling should maybe be based on setjmp longjmp (problem is allocations) + - If error user should get a NULL, context should have the line, column and error message + - Make sure there are no fixed size stack buffers to exploit (like keys being too big in json etc.) + - Add ability to redefine the allocator at compile time + - Add more examples, test more in practice and get more insights! + +*/ +#ifndef JSON_LIBRARY_HEADER +#define JSON_LIBRARY_HEADER + +#ifndef JSON_API +#define JSON_API +#endif + +typedef enum { + JSON_NULL, + JSON_BOOL, + JSON_ARRAY, + JSON_OBJECT, + JSON_NUMBER, + JSON_STRING, +} json_kind_t; + +typedef struct json_t json_t; +typedef struct json_object_t json_object_t; +typedef struct json_array_t json_array_t; +typedef struct json_kv_t json_kv_t; + +struct json_kv_t { + char *key; + json_t *value; +}; + +struct json_object_t { + json_kv_t *data; + int len; + int cap; +}; + +struct json_array_t { + json_t **data; + int len; + int cap; +}; + +struct json_t { + json_kind_t kind; + union { + int boolean; + double number; + char *string; + json_object_t object; + json_array_t array; + }; +}; + +JSON_API json_t *json_parse(const char *stream); +JSON_API char *json_serialize_compact(json_t *n); +JSON_API char *json_serialize_pretty(json_t *n, const char *new_line, const char *indent); +JSON_API char *json_summarize(json_t *n); +JSON_API void json_print(json_t *n); +JSON_API json_t *json_object_get(json_t *n, const char *key); +JSON_API json_t *json_array_get(json_t *n, int idx); +JSON_API json_t *jq(json_t *n, const char *fmt, ...); +JSON_API double jqn(json_t *n, const char *fmt, ...); +JSON_API char *jqs(json_t *n, const char *fmt, ...); +JSON_API int jql(json_t *n, const char *fmt, ...); +JSON_API void json_free(json_t *n); +JSON_API int json_equal(json_t *a, json_t *b); +JSON_API json_t *json_read(const char *filename); + +#endif // JSON_LIBRARY_HEADER + +#if defined(JSON_LIBRARY_IMPL) || defined(JSON_LIBRARY_MAIN) +#include +#include +#include +#include +#include + +#define JSON_ERROR_CAP 512 + +typedef struct json_parse_ctx_t json_parse_ctx_t; +struct json_parse_ctx_t { + const char *start; + const char *at; + int line; + int column; + int has_error; + char error[JSON_ERROR_CAP]; + json_t *result; +}; + +typedef struct json_sb_t json_sb_t; +struct json_sb_t { + char *data; + int len; + int cap; +}; + +static void *json_realloc(void *p, size_t n) { + void *r = realloc(p, n); + if (!r && n) { fprintf(stderr, "out of memory\n"); exit(1); } + return r; +} + +static json_t *json_new(json_kind_t kind) { + json_t *n = (json_t *)calloc(1, sizeof(json_t)); + if (!n) { fprintf(stderr, "out of memory\n"); exit(1); } + n->kind = kind; + return n; +} + +static void json_set_error(json_parse_ctx_t *ctx, const char *fmt, ...) { + if (ctx->has_error) return; + ctx->has_error = 1; + + int off = snprintf(ctx->error, sizeof(ctx->error), + "line %d, column %d: ", ctx->line, ctx->column); + if (off < 0) { + ctx->error[0] = 0; + return; + } + if (off >= (int)sizeof(ctx->error)) off = (int)sizeof(ctx->error) - 1; + + va_list ap; + va_start(ap, fmt); + vsnprintf(ctx->error + off, sizeof(ctx->error) - (size_t)off, fmt, ap); + va_end(ap); + ctx->error[sizeof(ctx->error) - 1] = 0; +} + +static void json_advance(json_parse_ctx_t *ctx) { + if (*ctx->at == '\n') { + ctx->line += 1; + ctx->column = 1; + } else { + ctx->column += 1; + } + ctx->at += 1; +} + +static void json_skip_ws(json_parse_ctx_t *ctx) { + while (*ctx->at && isspace((unsigned char)*ctx->at)) json_advance(ctx); +} + +static int json_match(json_parse_ctx_t *ctx, const char *s) { + const char *p = ctx->at; + while (*s) { + if (*p++ != *s++) return 0; + } + while (ctx->at < p) json_advance(ctx); + return 1; +} + +static void sb_init(json_sb_t *sb) { + sb->cap = 128; + sb->len = 0; + sb->data = (char *)json_realloc(NULL, sb->cap); + sb->data[0] = 0; +} + +static void sb_reserve(json_sb_t *sb, int extra) { + int need = sb->len + extra + 1; + if (need <= sb->cap) return; + while (sb->cap < need) sb->cap *= 2; + sb->data = (char *)json_realloc(sb->data, sb->cap); +} + +static void sb_putc(json_sb_t *sb, char c) { + sb_reserve(sb, 1); + sb->data[sb->len++] = c; + sb->data[sb->len] = 0; +} + +static void sb_puts(json_sb_t *sb, const char *s) { + int n = (int)strlen(s); + sb_reserve(sb, n); + memcpy(sb->data + sb->len, s, n + 1); + sb->len += n; +} + +static void sb_printf(json_sb_t *sb, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + va_list ap2; + va_copy(ap2, ap); + int n = vsnprintf(NULL, 0, fmt, ap); + va_end(ap); + sb_reserve(sb, n); + vsnprintf(sb->data + sb->len, sb->cap - sb->len, fmt, ap2); + va_end(ap2); + sb->len += n; +} + +static void sb_put_utf8(json_sb_t *sb, unsigned code) { + if (code <= 0x7f) { + sb_putc(sb, (char)code); + } else if (code <= 0x7ff) { + sb_putc(sb, (char)(0xc0 | (code >> 6))); + sb_putc(sb, (char)(0x80 | (code & 0x3f))); + } else { + sb_putc(sb, (char)(0xe0 | (code >> 12))); + sb_putc(sb, (char)(0x80 | ((code >> 6) & 0x3f))); + sb_putc(sb, (char)(0x80 | (code & 0x3f))); + } +} + +static int hexval(char c) { + if ('0' <= c && c <= '9') return c - '0'; + if ('a' <= c && c <= 'f') return c - 'a' + 10; + if ('A' <= c && c <= 'F') return c - 'A' + 10; + return -1; +} + +static json_t *json_parse_value(json_parse_ctx_t *ctx); + +static char *json_parse_string(json_parse_ctx_t *ctx) { + if (*ctx->at != '"') { json_set_error(ctx, "expected string"); return NULL; } + json_advance(ctx); + json_sb_t sb; + sb_init(&sb); + + while (*ctx->at && *ctx->at != '"') { + unsigned char c = (unsigned char)*ctx->at; + if (c < 0x20) { json_set_error(ctx, "control character in string"); free(sb.data); return NULL; } + if (c == '\\') { + json_advance(ctx); + switch (*ctx->at) { + case '"': sb_putc(&sb, '"'); json_advance(ctx); break; + case '\\': sb_putc(&sb, '\\'); json_advance(ctx); break; + case '/': sb_putc(&sb, '/'); json_advance(ctx); break; + case 'b': sb_putc(&sb, '\b'); json_advance(ctx); break; + case 'f': sb_putc(&sb, '\f'); json_advance(ctx); break; + case 'n': sb_putc(&sb, '\n'); json_advance(ctx); break; + case 'r': sb_putc(&sb, '\r'); json_advance(ctx); break; + case 't': sb_putc(&sb, '\t'); json_advance(ctx); break; + case 'u': { + json_advance(ctx); + unsigned code = 0; + for (int i = 0; i < 4; i += 1) { + int h = hexval(*ctx->at); + if (h < 0) { json_set_error(ctx, "bad unicode escape"); free(sb.data); return NULL; } + code = (code << 4) | (unsigned)h; + json_advance(ctx); + } + sb_put_utf8(&sb, code); + } break; + default: json_set_error(ctx, "bad escape sequence"); free(sb.data); return NULL; + } + } else { + sb_putc(&sb, (char)c); + json_advance(ctx); + } + } + if (*ctx->at != '"') { json_set_error(ctx, "unterminated string"); free(sb.data); return NULL; } + json_advance(ctx); + return sb.data; +} + +static json_t *json_parse_number(json_parse_ctx_t *ctx) { + char *end = NULL; + double v = strtod(ctx->at, &end); + if (end == ctx->at) { json_set_error(ctx, "expected number"); return NULL; } + while (ctx->at < end) json_advance(ctx); + json_t *n = json_new(JSON_NUMBER); + n->number = v; + return n; +} + +static void json_array_push(json_array_t *a, json_t *v) { + if (a->len == a->cap) { + a->cap = a->cap ? a->cap * 2 : 8; + a->data = (json_t **)json_realloc(a->data, sizeof(json_t *) * a->cap); + } + a->data[a->len++] = v; +} + +static void json_object_put(json_object_t *o, char *key, json_t *value) { + if (o->len == o->cap) { + o->cap = o->cap ? o->cap * 2 : 8; + o->data = (json_kv_t *)json_realloc(o->data, sizeof(json_kv_t) * o->cap); + } + o->data[o->len].key = key; + o->data[o->len].value = value; + o->len += 1; +} + +static int json_parse_next(json_parse_ctx_t *ctx, char close, const char *what) { + json_skip_ws(ctx); + if (*ctx->at == close) { json_advance(ctx); return 0; } + if (*ctx->at != ',') { json_set_error(ctx, "expected ',' or '%c' in %s", close, what); return -1; } + json_advance(ctx); + json_skip_ws(ctx); + return 1; +} + +static json_t *json_parse_array(json_parse_ctx_t *ctx) { + json_t *n = json_new(JSON_ARRAY); + json_advance(ctx); // [ + json_skip_ws(ctx); + if (*ctx->at == ']') { json_advance(ctx); return n; } + for (;;) { + json_t *v = json_parse_value(ctx); + if (!v) return n; + json_array_push(&n->array, v); + int next = json_parse_next(ctx, ']', "array"); + if (next <= 0) return n; + } +} + +static json_t *json_parse_object(json_parse_ctx_t *ctx) { + json_t *n = json_new(JSON_OBJECT); + json_advance(ctx); // { + json_skip_ws(ctx); + if (*ctx->at == '}') { json_advance(ctx); return n; } + for (;;) { + if (*ctx->at != '"') { json_set_error(ctx, "expected object key string"); return n; } + char *key = json_parse_string(ctx); + if (!key) return n; + json_skip_ws(ctx); + if (*ctx->at != ':') { json_set_error(ctx, "expected ':' after object key"); free(key); return n; } + json_advance(ctx); + json_t *value = json_parse_value(ctx); + if (!value) { free(key); return n; } + json_object_put(&n->object, key, value); + int next = json_parse_next(ctx, '}', "object"); + if (next <= 0) return n; + } +} + +static json_t *json_parse_value(json_parse_ctx_t *ctx) { + json_skip_ws(ctx); + switch (*ctx->at) { + case 'n': + if (json_match(ctx, "null")) return json_new(JSON_NULL); + break; + case 't': + if (json_match(ctx, "true")) { json_t *n = json_new(JSON_BOOL); n->boolean = 1; return n; } + break; + case 'f': + if (json_match(ctx, "false")) { json_t *n = json_new(JSON_BOOL); n->boolean = 0; return n; } + break; + case '"': { + json_t *n = json_new(JSON_STRING); + n->string = json_parse_string(ctx); + if (!n->string) { free(n); return NULL; } + return n; + } + case '[': return json_parse_array(ctx); + case '{': return json_parse_object(ctx); + default: + if (*ctx->at == '-' || isdigit((unsigned char)*ctx->at)) return json_parse_number(ctx); + break; + } + json_set_error(ctx, "expected JSON value"); + return NULL; +} + +static void json_parse_ex(json_parse_ctx_t *ctx) { + ctx->line = ctx->line ? ctx->line : 1; + ctx->column = ctx->column ? ctx->column : 1; + ctx->result = json_parse_value(ctx); + if (ctx->has_error) return; + json_skip_ws(ctx); + if (*ctx->at) json_set_error(ctx, "unexpected trailing data"); +} + +JSON_API json_t *json_parse(const char *stream) { + json_parse_ctx_t ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.start = stream; + ctx.at = stream; + ctx.line = 1; + ctx.column = 1; + json_parse_ex(&ctx); + if (ctx.has_error) { + fprintf(stderr, "json parse error: %s\n", ctx.error); + json_free(ctx.result); + return NULL; + } + return ctx.result; +} + +static void json_escape_string(json_sb_t *sb, const char *s) { + sb_putc(sb, '"'); + for (; *s; s += 1) { + unsigned char c = (unsigned char)*s; + switch (c) { + case '"': sb_puts(sb, "\\\""); break; + case '\\': sb_puts(sb, "\\\\"); break; + case '\b': sb_puts(sb, "\\b"); break; + case '\f': sb_puts(sb, "\\f"); break; + case '\n': sb_puts(sb, "\\n"); break; + case '\r': sb_puts(sb, "\\r"); break; + case '\t': sb_puts(sb, "\\t"); break; + default: + if (c < 0x20) sb_printf(sb, "\\u%04x", c); + else sb_putc(sb, (char)c); + } + } + sb_putc(sb, '"'); +} + +static void json_indent(json_sb_t *sb, const char *nl, const char *indent, int depth) { + if (!nl) return; + sb_puts(sb, nl); + for (int i = 0; i < depth; i += 1) sb_puts(sb, indent); +} + +static void json_serialize_into(json_sb_t *sb, json_t *n, const char *nl, const char *indent, int depth) { + if (!n) { sb_puts(sb, "null"); return; } + switch (n->kind) { + case JSON_NULL: sb_puts(sb, "null"); break; + case JSON_BOOL: sb_puts(sb, n->boolean ? "true" : "false"); break; + case JSON_NUMBER: sb_printf(sb, "%.17g", n->number); break; + case JSON_STRING: json_escape_string(sb, n->string ? n->string : ""); break; + case JSON_ARRAY: + sb_putc(sb, '['); + for (int i = 0; i < n->array.len; i += 1) { + if (i) sb_putc(sb, ','); + json_indent(sb, nl, indent, depth + 1); + json_serialize_into(sb, n->array.data[i], nl, indent, depth + 1); + } + if (n->array.len) json_indent(sb, nl, indent, depth); + sb_putc(sb, ']'); + break; + case JSON_OBJECT: + sb_putc(sb, '{'); + for (int i = 0; i < n->object.len; i += 1) { + if (i) sb_putc(sb, ','); + json_indent(sb, nl, indent, depth + 1); + json_escape_string(sb, n->object.data[i].key); + sb_putc(sb, ':'); + if (nl) sb_putc(sb, ' '); + json_serialize_into(sb, n->object.data[i].value, nl, indent, depth + 1); + } + if (n->object.len) json_indent(sb, nl, indent, depth); + sb_putc(sb, '}'); + break; + } +} + +JSON_API char *json_serialize_compact(json_t *n) { + json_sb_t sb; + sb_init(&sb); + json_serialize_into(&sb, n, NULL, NULL, 0); + return sb.data; +} + +JSON_API char *json_serialize_pretty(json_t *n, const char *new_line, const char *indent) { + json_sb_t sb; + sb_init(&sb); + json_serialize_into(&sb, n, new_line ? new_line : "\n", indent ? indent : " ", 0); + return sb.data; +} + +JSON_API void json_print(json_t *n) { + char *s = json_serialize_pretty(n, "\n", " "); + puts(s); + free(s); +} + +static const char *json_kind_name(json_kind_t kind) { + switch (kind) { + case JSON_NULL: return "null"; + case JSON_BOOL: return "bool"; + case JSON_ARRAY: return "array"; + case JSON_OBJECT: return "object"; + case JSON_NUMBER: return "number"; + case JSON_STRING: return "string"; + } + return "unknown"; +} + +static void json_summary_indent(json_sb_t *sb, int depth) { + for (int i = 0; i < depth; i += 1) sb_puts(sb, " "); +} + +static void json_summary_string(json_sb_t *sb, const char *s) { + int limit = 60; + sb_putc(sb, '"'); + for (int i = 0; s && s[i] && i < limit; i += 1) { + unsigned char c = (unsigned char)s[i]; + switch (c) { + case '"': sb_puts(sb, "\\\""); break; + case '\\': sb_puts(sb, "\\\\"); break; + case '\n': sb_puts(sb, "\\n"); break; + case '\r': sb_puts(sb, "\\r"); break; + case '\t': sb_puts(sb, "\\t"); break; + default: if (c < 0x20) sb_printf(sb, "\\u%04x", c); else sb_putc(sb, (char)c); + } + } + if (s && (int)strlen(s) > limit) sb_puts(sb, "..."); + sb_putc(sb, '"'); +} + +static int json_summary_kind_count(json_t *n, int counts[6]) { + int kinds = 0; + memset(counts, 0, sizeof(int) * 6); + if (!n || n->kind != JSON_ARRAY) return 0; + for (int i = 0; i < n->array.len; i += 1) { + json_kind_t k = n->array.data[i] ? n->array.data[i]->kind : JSON_NULL; + if (counts[k]++ == 0) kinds += 1; + } + return kinds; +} + +static void json_summarize_into(json_sb_t *sb, json_t *n, int depth, int max_depth); + +static void json_summarize_array(json_sb_t *sb, json_t *n, int depth, int max_depth) { + int counts[6]; + int kinds = json_summary_kind_count(n, counts); + sb_printf(sb, "array[%d]", n->array.len); + if (n->array.len == 0) { sb_putc(sb, '\n'); return; } + + if (kinds == 1 && counts[JSON_NUMBER]) { + double min = n->array.data[0]->number, max = min, sum = 0.0; + for (int i = 0; i < n->array.len; i += 1) { + double v = n->array.data[i]->number; + if (v < min) min = v; + if (v > max) max = v; + sum += v; + } + sb_printf(sb, " of number { min: %.17g, max: %.17g, avg: %.17g, first: %.17g, last: %.17g }\n", + min, max, sum / n->array.len, n->array.data[0]->number, n->array.data[n->array.len - 1]->number); + return; + } + + if (kinds <= 2 && counts[JSON_NUMBER] && counts[JSON_NULL]) { + int nums = 0; + double min = 0.0, max = 0.0, sum = 0.0, first = 0.0, last = 0.0; + for (int i = 0; i < n->array.len; i += 1) if (n->array.data[i] && n->array.data[i]->kind == JSON_NUMBER) { + double v = n->array.data[i]->number; + if (nums == 0) min = max = first = v; + if (v < min) min = v; + if (v > max) max = v; + sum += v; + last = v; + nums += 1; + } + sb_printf(sb, " of number|null { numbers: %d, nulls: %d, min: %.17g, max: %.17g, avg: %.17g, first-number: %.17g, last-number: %.17g }\n", + nums, counts[JSON_NULL], min, max, nums ? sum / nums : 0.0, first, last); + return; + } + + if (kinds == 1) sb_printf(sb, " of %s", json_kind_name(n->array.data[0] ? n->array.data[0]->kind : JSON_NULL)); + else { + sb_puts(sb, " mixed {"); + for (int k = 0; k < 6; k += 1) if (counts[k]) sb_printf(sb, " %s:%d", json_kind_name((json_kind_t)k), counts[k]); + sb_puts(sb, " }"); + } + + if (depth >= max_depth) { sb_puts(sb, " { ... }\n"); return; } + sb_puts(sb, " {\n"); + int samples = n->array.len < 3 ? n->array.len : 3; + for (int i = 0; i < samples; i += 1) { + json_summary_indent(sb, depth + 1); + sb_printf(sb, "[%d]: ", i); + json_summarize_into(sb, n->array.data[i], depth + 1, max_depth); + } + if (n->array.len > samples) { + json_summary_indent(sb, depth + 1); + sb_printf(sb, "... %d more\n", n->array.len - samples); + } + json_summary_indent(sb, depth); + sb_puts(sb, "}\n"); +} + +static void json_summarize_into(json_sb_t *sb, json_t *n, int depth, int max_depth) { + if (!n) { sb_puts(sb, "null\n"); return; } + switch (n->kind) { + case JSON_NULL: sb_puts(sb, "null\n"); break; + case JSON_BOOL: sb_printf(sb, "bool %s\n", n->boolean ? "true" : "false"); break; + case JSON_NUMBER: sb_printf(sb, "number %.17g\n", n->number); break; + case JSON_STRING: sb_puts(sb, "string "); json_summary_string(sb, n->string ? n->string : ""); sb_putc(sb, '\n'); break; + case JSON_ARRAY: json_summarize_array(sb, n, depth, max_depth); break; + case JSON_OBJECT: { + sb_printf(sb, "object[%d]", n->object.len); + if (depth >= max_depth) { sb_puts(sb, " { ... }\n"); break; } + sb_puts(sb, " {\n"); + int limit = n->object.len < 32 ? n->object.len : 32; + for (int i = 0; i < limit; i += 1) { + json_summary_indent(sb, depth + 1); + sb_puts(sb, n->object.data[i].key); + sb_puts(sb, ": "); + json_summarize_into(sb, n->object.data[i].value, depth + 1, max_depth); + } + if (n->object.len > limit) { + json_summary_indent(sb, depth + 1); + sb_printf(sb, "... %d more keys\n", n->object.len - limit); + } + json_summary_indent(sb, depth); + sb_puts(sb, "}\n"); + } break; + } +} + +JSON_API char *json_summarize(json_t *n) { + json_sb_t sb; + sb_init(&sb); + json_summarize_into(&sb, n, 0, 8); + return sb.data; +} + +JSON_API json_t *json_object_get(json_t *n, const char *key) { + if (!n || n->kind != JSON_OBJECT) return NULL; + for (int i = 0; i < n->object.len; i += 1) { + if (strcmp(n->object.data[i].key, key) == 0) return n->object.data[i].value; + } + return NULL; +} + +static json_t *json_object_get_n(json_t *n, const char *key, size_t key_len) { + if (!n || n->kind != JSON_OBJECT) return NULL; + for (int i = 0; i < n->object.len; i += 1) { + const char *candidate = n->object.data[i].key; + if (strlen(candidate) == key_len && memcmp(candidate, key, key_len) == 0) { + return n->object.data[i].value; + } + } + return NULL; +} + +JSON_API json_t *json_array_get(json_t *n, int idx) { + if (!n || n->kind != JSON_ARRAY || idx < 0 || idx >= n->array.len) return NULL; + return n->array.data[idx]; +} + +static json_t *jqv(json_t *n, const char *fmt, va_list ap) { + va_list ap2; + va_copy(ap2, ap); + int path_len = vsnprintf(NULL, 0, fmt, ap2); + va_end(ap2); + if (path_len < 0) return NULL; + + char *path = (char *)json_realloc(NULL, (size_t)path_len + 1); + vsnprintf(path, (size_t)path_len + 1, fmt, ap); + + const char *p = path; + json_t *cur = n; + + while (cur && *p) { + if (*p == '.') { p += 1; continue; } + if (*p == '[') { + p += 1; + int idx = (int)strtol(p, (char **)&p, 10); + if (*p++ != ']') { cur = NULL; break; } + cur = json_array_get(cur, idx); + } else { + const char *key = p; + while (*p && *p != '.' && *p != '[') p += 1; + cur = json_object_get_n(cur, key, (size_t)(p - key)); + } + } + + free(path); + return cur; +} + +// Query with dotted object fields and bracket array indexes: root.items[0].name +JSON_API json_t *jq(json_t *n, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + json_t *r = jqv(n, fmt, ap); + va_end(ap); + return r; +} + +JSON_API double jqn(json_t *n, const char *fmt, ...) { // extract number from query + va_list ap; + va_start(ap, fmt); + json_t *r = jqv(n, fmt, ap); + va_end(ap); + return (r && r->kind == JSON_NUMBER) ? r->number : 0.0; +} + +JSON_API char *jqs(json_t *n, const char *fmt, ...) { // extract string from query + va_list ap; + va_start(ap, fmt); + json_t *r = jqv(n, fmt, ap); + va_end(ap); + return (r && r->kind == JSON_STRING) ? r->string : NULL; +} + +JSON_API int jql(json_t *n, const char *fmt, ...) { // extract len of array or object from query + va_list ap; + va_start(ap, fmt); + json_t *r = jqv(n, fmt, ap); + va_end(ap); + if (!r) return 0; + if (r->kind == JSON_ARRAY) return r->array.len; + if (r->kind == JSON_OBJECT) return r->object.len; + return 0; +} + +JSON_API void json_free(json_t *n) { + if (!n) return; + switch (n->kind) { + case JSON_STRING: free(n->string); break; + case JSON_ARRAY: + for (int i = 0; i < n->array.len; i += 1) json_free(n->array.data[i]); + free(n->array.data); + break; + case JSON_OBJECT: + for (int i = 0; i < n->object.len; i += 1) { + free(n->object.data[i].key); + json_free(n->object.data[i].value); + } + free(n->object.data); + break; + default: break; + } + free(n); +} + +JSON_API int json_equal(json_t *a, json_t *b) { + if (a == b) return 1; + if (!a || !b || a->kind != b->kind) return 0; + + switch (a->kind) { + case JSON_NULL: return 1; + case JSON_BOOL: return a->boolean == b->boolean; + case JSON_NUMBER: return a->number == b->number; + case JSON_STRING: return strcmp(a->string ? a->string : "", b->string ? b->string : "") == 0; + case JSON_ARRAY: + if (a->array.len != b->array.len) return 0; + for (int i = 0; i < a->array.len; i += 1) { + if (!json_equal(a->array.data[i], b->array.data[i])) return 0; + } + return 1; + case JSON_OBJECT: + if (a->object.len != b->object.len) return 0; + for (int i = 0; i < a->object.len; i += 1) { + // Serialization preserves object member order, so compare in order. + if (strcmp(a->object.data[i].key, b->object.data[i].key) != 0) return 0; + if (!json_equal(a->object.data[i].value, b->object.data[i].value)) return 0; + } + return 1; + } + return 0; +} + +static char *json_read_entire_file(const char *path) { + FILE *f = fopen(path, "rb"); + if (!f) return NULL; + + if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return NULL; } + long size = ftell(f); + if (size < 0) { fclose(f); return NULL; } + rewind(f); + + char *data = (char *)json_realloc(NULL, (size_t)size + 1); + size_t got = fread(data, 1, (size_t)size, f); + fclose(f); + data[got] = 0; + return data; +} + +JSON_API json_t *json_read(const char *filename) { + char *stream = json_read_entire_file(filename); + if (!stream) return NULL; + json_t *result = json_parse(stream); + free(stream); + return result; +} + +#endif +#ifdef JSON_LIBRARY_MAIN + +int test_read_acwi(void) { + printf("\n-- test_read_acwi --\n"); + + char *stream = json_read_entire_file("acwi.json"); + if (!stream) { + fprintf(stderr, "could not read ./acwi.json\n"); + return 0; + } + + json_t *root = json_parse(stream); + free(stream); + if (!root) return 0; + + char *serialized = json_serialize_compact(root); + json_t *roundtrip = json_parse(serialized); + if (!roundtrip) { + fprintf(stderr, "could not parse serialized acwi json\n"); + free(serialized); + json_free(root); + return 0; + } + if (!json_equal(root, roundtrip)) { + fprintf(stderr, "acwi json was not equal after parse -> serialize -> parse\n"); + free(serialized); + json_free(roundtrip); + json_free(root); + return 0; + } + printf("roundtrip parse/serialize/parse equality: ok (%d bytes serialized)\n", (int)strlen(serialized)); + free(serialized); + json_free(roundtrip); + + char *symbol = jqs(root, "chart.result[0].meta.symbol"); + char *name = jqs(root, "chart.result[0].meta.longName"); + double market_price = jqn(root, "chart.result[0].meta.regularMarketPrice"); + int timestamp_count = jql(root, "chart.result[0].timestamp"); + + json_t *closes = jq(root, "chart.result[0].indicators.quote[0].close"); + if (!symbol || strcmp(symbol, "ACWI") != 0 || !closes || closes->kind != JSON_ARRAY || timestamp_count <= 0) { + fprintf(stderr, "acwi json did not have the expected Yahoo chart shape\n"); + json_free(root); + return 0; + } + + double min_close = 0.0, max_close = 0.0, last_close = 0.0, sum_close = 0.0; + int close_count = 0; + for (int i = 0; i < closes->array.len; i += 1) { + json_t *it = closes->array.data[i]; + if (!it || it->kind != JSON_NUMBER) continue; // Yahoo may include nulls. + double v = it->number; + if (close_count == 0 || v < min_close) min_close = v; + if (close_count == 0 || v > max_close) max_close = v; + last_close = v; + sum_close += v; + close_count += 1; + } + + if (close_count == 0) { + fprintf(stderr, "acwi close array had no numeric prices\n"); + json_free(root); + return 0; + } + + printf("symbol: %s\n", symbol); + printf("name: %s\n", name ? name : "(missing)"); + printf("regularMarketPrice: %.2f\n", market_price); + printf("timestamps: %d, closes: %d numeric / %d total\n", timestamp_count, close_count, closes->array.len); + printf("close min/avg/max/last: %.2f / %.2f / %.2f / %.2f\n", + min_close, sum_close / close_count, max_close, last_close); + + json_free(root); + return 1; +} + +int test_simple_json(void) { + printf("\n-- test_simple_json --\n"); + + const char *json = + "{" + "\"name\":\"example\"," + "\"version\":1," + "\"enabled\":true," + "\"items\":[\"alpha\",\"beta\",\"gamma\"]," + "\"config\":{" + "\"timeout_ms\":5000," + "\"retry_count\":3" + "}," + "\"message\":\"Hello, \\\"world\\\"!\"" + "}"; + json_t *n = json_parse(json); + if (!n) return 0; + + for (int i = 0; i < n->object.len; i += 1) { + json_kv_t *it = n->object.data + i; + printf("%s\n", it->key); + } + + char *name = jqs(n, "name"); + double version = jqn(n, "version"); + double timeout = jqn(n, "config.timeout_ms"); + printf("name=%s version=%.0f timeout=%.0f\n", name, version, timeout); + + printf("items = {\n"); + for (int i = 0; i < jql(n, "items"); i += 1) { + char *it = jqs(n, "items[%d]", i); + printf(" %s\n", it); + } + printf("}\n"); + + char *pretty = json_serialize_pretty(n, "\n", " "); + puts(pretty); + free(pretty); + json_free(n); + return 1; +} + +int test_large_parse_error_buffer(void) { + printf("\n-- test_large_parse_error_buffer --\n"); + + size_t len = 2048; + char *json = (char *)json_realloc(NULL, len + 3); + json[0] = '"'; + memset(json + 1, 'A', len); + json[len + 1] = 0; // Deliberately no closing quote. + + json_t *n = json_parse(json); + free(json); + if (n) { + fprintf(stderr, "unterminated string larger than 512 bytes parsed unexpectedly\n"); + json_free(n); + return 0; + } + + printf("large unterminated string parse error: ok\n"); + return 1; +} + +int test_large_query_key(void) { + printf("\n-- test_large_query_key --\n"); + + size_t key_len = 768; + char *json = (char *)json_realloc(NULL, key_len + 32); + char *query = (char *)json_realloc(NULL, key_len + 1); + memset(query, 'k', key_len); + query[key_len] = 0; + + json[0] = '{'; + json[1] = '"'; + memcpy(json + 2, query, key_len); + memcpy(json + 2 + key_len, "\":12345}", 9); + + json_t *root = json_parse(json); + if (!root) { + fprintf(stderr, "could not parse object with key larger than 512 bytes\n"); + free(query); + free(json); + return 0; + } + + double value = jqn(root, "%s", query); + int ok = value == 12345.0; + if (!ok) fprintf(stderr, "query with key larger than 512 bytes failed\n"); + + json_free(root); + free(query); + free(json); + printf("large query key: %s\n", ok ? "ok" : "failed"); + return ok; +} + +char *json_read_stream(FILE *f) { + json_sb_t sb; + sb_init(&sb); + char buf[4096]; + for (;;) { + size_t n = fread(buf, 1, sizeof(buf), f); + if (n > 0) { + sb_reserve(&sb, (int)n); + memcpy(sb.data + sb.len, buf, n); + sb.len += (int)n; + sb.data[sb.len] = 0; + } + if (n < sizeof(buf)) { + if (ferror(f)) { free(sb.data); return NULL; } + break; + } + } + return sb.data; +} + +void usage(const char *argv0) { + fprintf(stderr, + "usage:\n" + " %s --test\n" + " %s [query] [file]\n" + " %s --summary [query] [file]\n" + "\n" + "Reads JSON from file, or stdin when file is omitted.\n" + "Query examples: name, items[0], chart.result[0].meta.symbol\n", + argv0, argv0, argv0); +} + +int run_tests(void) { + int ok = 1; + ok = test_simple_json() && ok; + ok = test_large_parse_error_buffer() && ok; + ok = test_large_query_key() && ok; + ok = test_read_acwi() && ok; + printf("\n%s\n", ok ? "all tests passed" : "some tests failed"); + return ok ? 0 : 1; +} + +int main(int argc, char **argv) { + if (argc >= 2 && strcmp(argv[1], "--test") == 0) return run_tests(); + + int summary = 0; + int argi = 1; + if (argc >= 2 && strcmp(argv[1], "--summary") == 0) { + summary = 1; + argi = 2; + } + + if (argc > argi + 2 || (argc >= 2 && strcmp(argv[1], "--help") == 0)) { + usage(argv[0]); + return argc > argi + 2 ? 1 : 0; + } + + const char *query = argc > argi ? argv[argi] : NULL; + const char *path = argc > argi + 1 ? argv[argi + 1] : NULL; + + char *stream = NULL; + if (path) { + stream = json_read_entire_file(path); + } else if (summary && query) { + stream = json_read_entire_file(query); + if (stream) { path = query; query = NULL; } + } + if (!stream) stream = json_read_stream(stdin); + if (!stream) { + fprintf(stderr, "could not read %s\n", path ? path : "stdin"); + return 1; + } + + json_t *root = json_parse(stream); + free(stream); + if (!root) return 1; + + json_t *selected = query && *query ? jq(root, "%s", query) : root; + if (!selected) { + fprintf(stderr, "query did not match: %s\n", query); + json_free(root); + return 1; + } + + char *pretty = summary ? json_summarize(selected) : json_serialize_pretty(selected, "\n", " "); + fputs(pretty, stdout); + if (!summary) putchar('\n'); + free(pretty); + json_free(root); + return 0; +} + +#endif