1126 lines
36 KiB
C
Executable File
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
|