//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