Files
dotfiles/bin/json.c
2026-06-25 08:37:18 +02:00

1126 lines
36 KiB
C
Executable File

//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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <ctype.h>
#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