calog/libs/calogJson.c
2026-07-03 02:13:23 -05:00

762 lines
22 KiB
C

// calogJson.c -- calog JSON library (see calogJson.h). A self-contained recursive-descent
// parser and a serializer, both bridging JSON text and calog's value/aggregate model. No
// shared state: the two natives are pure functions of their arguments.
#define _POSIX_C_SOURCE 200809L
#include "calogJson.h"
#include <errno.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define JSON_BUF_INITIAL 64
// A growable output byte buffer for serialization and for decoded string bodies.
typedef struct JsonBufT {
char *bytes;
size_t length;
size_t cap;
} JsonBufT;
// A read cursor over the source text (binary-safe: bounded by end, not a NUL).
typedef struct JsonParseT {
const char *cur;
const char *end;
} JsonParseT;
static int32_t jsonBufEnsure(JsonBufT *buf, size_t extra);
static void jsonBufFree(JsonBufT *buf);
static int32_t jsonBufPutBytes(JsonBufT *buf, const char *bytes, size_t length);
static int32_t jsonBufPutChar(JsonBufT *buf, char c);
static int32_t jsonEncode(JsonBufT *buf, const CalogValueT *value, int32_t depth);
static int32_t jsonEncodeAgg(JsonBufT *buf, const CalogAggT *agg, int32_t depth);
static int32_t jsonEncodeKey(JsonBufT *buf, const CalogValueT *key);
static int32_t jsonEncodeReal(JsonBufT *buf, double r);
static int32_t jsonEncodeString(JsonBufT *buf, const char *bytes, int64_t length);
static int32_t jsonHexQuad(JsonParseT *p, uint32_t *out);
static int32_t jsonParseArray(JsonParseT *p, CalogValueT *out, int32_t depth);
static int32_t jsonParseLiteral(JsonParseT *p, CalogValueT *out);
static int32_t jsonParseNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t jsonParseNumber(JsonParseT *p, CalogValueT *out);
static int32_t jsonParseObject(JsonParseT *p, CalogValueT *out, int32_t depth);
static int32_t jsonParseString(JsonParseT *p, CalogValueT *out);
static int32_t jsonParseUnicode(JsonParseT *p, JsonBufT *buf);
static int32_t jsonParseValue(JsonParseT *p, CalogValueT *out, int32_t depth);
static int32_t jsonPutUtf8(JsonBufT *buf, uint32_t cp);
static void jsonSkipWs(JsonParseT *p);
static int32_t jsonStringifyNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
int32_t calogJsonRegister(CalogT *calog) {
calogRegisterInline(calog, "jsonParse", jsonParseNative, NULL);
calogRegisterInline(calog, "jsonStringify", jsonStringifyNative, NULL);
return calogOkE;
}
static int32_t jsonBufEnsure(JsonBufT *buf, size_t extra) {
size_t need;
size_t cap;
char *grown;
need = buf->length + extra;
if (need <= buf->cap) {
return calogOkE;
}
cap = (buf->cap == 0) ? JSON_BUF_INITIAL : buf->cap;
while (cap < need) {
cap *= CALOG_GROWTH_FACTOR;
}
grown = (char *)realloc(buf->bytes, cap);
if (grown == NULL) {
return calogErrOomE;
}
buf->bytes = grown;
buf->cap = cap;
return calogOkE;
}
static void jsonBufFree(JsonBufT *buf) {
free(buf->bytes);
buf->bytes = NULL;
buf->length = 0;
buf->cap = 0;
}
static int32_t jsonBufPutBytes(JsonBufT *buf, const char *bytes, size_t length) {
int32_t status;
if (length == 0) {
return calogOkE;
}
status = jsonBufEnsure(buf, length);
if (status != calogOkE) {
return status;
}
memcpy(buf->bytes + buf->length, bytes, length);
buf->length += length;
return calogOkE;
}
static int32_t jsonBufPutChar(JsonBufT *buf, char c) {
return jsonBufPutBytes(buf, &c, 1);
}
static int32_t jsonEncode(JsonBufT *buf, const CalogValueT *value, int32_t depth) {
char tmp[32];
int n;
if (depth >= CALOG_MAX_DEPTH) {
return calogErrDepthE;
}
switch (value->type) {
case calogNilE:
return jsonBufPutBytes(buf, "null", 4);
case calogBoolE:
return value->as.b ? jsonBufPutBytes(buf, "true", 4) : jsonBufPutBytes(buf, "false", 5);
case calogIntE:
n = snprintf(tmp, sizeof(tmp), "%lld", (long long)value->as.i);
return jsonBufPutBytes(buf, tmp, (size_t)n);
case calogRealE:
return jsonEncodeReal(buf, value->as.r);
case calogStringE:
return jsonEncodeString(buf, value->as.s.bytes, value->as.s.length);
case calogAggE:
return jsonEncodeAgg(buf, value->as.agg, depth);
case calogFnE:
return calogErrTypeE;
default:
return calogErrTypeE;
}
}
static int32_t jsonEncodeAgg(JsonBufT *buf, const CalogAggT *agg, int32_t depth) {
int64_t index;
int32_t status;
// A map (or a hybrid that carries keyed pairs) serializes as a JSON object; a pure
// sequence serializes as a JSON array. In the hybrid case the array elements are emitted
// under their integer indices so nothing is silently dropped.
if (agg->pairCount > 0 || agg->kind == calogMapE) {
bool first;
first = true;
status = jsonBufPutChar(buf, '{');
if (status != calogOkE) {
return status;
}
for (index = 0; index < agg->arrayCount; index++) {
char tmp[32];
int n;
if (!first) {
status = jsonBufPutChar(buf, ',');
if (status != calogOkE) {
return status;
}
}
first = false;
n = snprintf(tmp, sizeof(tmp), "\"%lld\":", (long long)index);
status = jsonBufPutBytes(buf, tmp, (size_t)n);
if (status != calogOkE) {
return status;
}
status = jsonEncode(buf, &agg->array[index], depth + 1);
if (status != calogOkE) {
return status;
}
}
for (index = 0; index < agg->pairCount; index++) {
if (!first) {
status = jsonBufPutChar(buf, ',');
if (status != calogOkE) {
return status;
}
}
first = false;
status = jsonEncodeKey(buf, &agg->pairs[index].key);
if (status != calogOkE) {
return status;
}
status = jsonBufPutChar(buf, ':');
if (status != calogOkE) {
return status;
}
status = jsonEncode(buf, &agg->pairs[index].value, depth + 1);
if (status != calogOkE) {
return status;
}
}
return jsonBufPutChar(buf, '}');
}
status = jsonBufPutChar(buf, '[');
if (status != calogOkE) {
return status;
}
for (index = 0; index < agg->arrayCount; index++) {
if (index > 0) {
status = jsonBufPutChar(buf, ',');
if (status != calogOkE) {
return status;
}
}
status = jsonEncode(buf, &agg->array[index], depth + 1);
if (status != calogOkE) {
return status;
}
}
return jsonBufPutChar(buf, ']');
}
static int32_t jsonEncodeKey(JsonBufT *buf, const CalogValueT *key) {
char tmp[32];
int n;
// JSON object keys must be strings; coerce a scalar key to its string form.
switch (key->type) {
case calogStringE:
return jsonEncodeString(buf, key->as.s.bytes, key->as.s.length);
case calogIntE:
n = snprintf(tmp, sizeof(tmp), "\"%lld\"", (long long)key->as.i);
return jsonBufPutBytes(buf, tmp, (size_t)n);
case calogBoolE:
return key->as.b ? jsonBufPutBytes(buf, "\"true\"", 6) : jsonBufPutBytes(buf, "\"false\"", 7);
case calogRealE:
n = snprintf(tmp, sizeof(tmp), "\"%.17g\"", key->as.r);
return jsonBufPutBytes(buf, tmp, (size_t)n);
default:
return calogErrTypeE;
}
}
static int32_t jsonEncodeReal(JsonBufT *buf, double r) {
char tmp[40];
int n;
if (!isfinite(r)) {
return jsonBufPutBytes(buf, "null", 4);
}
n = snprintf(tmp, sizeof(tmp), "%.15g", r);
if (strtod(tmp, NULL) != r) {
n = snprintf(tmp, sizeof(tmp), "%.17g", r);
}
return jsonBufPutBytes(buf, tmp, (size_t)n);
}
static int32_t jsonEncodeString(JsonBufT *buf, const char *bytes, int64_t length) {
int64_t index;
int32_t status;
status = jsonBufPutChar(buf, '"');
if (status != calogOkE) {
return status;
}
for (index = 0; index < length; index++) {
unsigned char c;
c = (unsigned char)bytes[index];
switch (c) {
case '"':
status = jsonBufPutBytes(buf, "\\\"", 2);
break;
case '\\':
status = jsonBufPutBytes(buf, "\\\\", 2);
break;
case '\b':
status = jsonBufPutBytes(buf, "\\b", 2);
break;
case '\f':
status = jsonBufPutBytes(buf, "\\f", 2);
break;
case '\n':
status = jsonBufPutBytes(buf, "\\n", 2);
break;
case '\r':
status = jsonBufPutBytes(buf, "\\r", 2);
break;
case '\t':
status = jsonBufPutBytes(buf, "\\t", 2);
break;
default:
if (c < 0x20) {
char esc[8];
int n;
n = snprintf(esc, sizeof(esc), "\\u%04x", (unsigned)c);
status = jsonBufPutBytes(buf, esc, (size_t)n);
} else {
status = jsonBufPutChar(buf, (char)c);
}
break;
}
if (status != calogOkE) {
return status;
}
}
return jsonBufPutChar(buf, '"');
}
static int32_t jsonHexQuad(JsonParseT *p, uint32_t *out) {
uint32_t value;
int i;
value = 0;
for (i = 0; i < 4; i++) {
int digit;
if (p->cur >= p->end) {
return calogErrArgE;
}
if (*p->cur >= '0' && *p->cur <= '9') {
digit = *p->cur - '0';
} else if (*p->cur >= 'a' && *p->cur <= 'f') {
digit = *p->cur - 'a' + 10;
} else if (*p->cur >= 'A' && *p->cur <= 'F') {
digit = *p->cur - 'A' + 10;
} else {
return calogErrArgE;
}
value = (value << 4) | (uint32_t)digit;
p->cur++;
}
*out = value;
return calogOkE;
}
static int32_t jsonParseArray(JsonParseT *p, CalogValueT *out, int32_t depth) {
CalogAggT *agg;
int32_t status;
calogValueNil(out);
status = calogAggCreate(&agg, calogListE);
if (status != calogOkE) {
return status;
}
p->cur++; // consume '['
jsonSkipWs(p);
if (p->cur < p->end && *p->cur == ']') {
p->cur++;
calogValueAgg(out, agg);
return calogOkE;
}
for (;;) {
CalogValueT element;
status = jsonParseValue(p, &element, depth + 1);
if (status != calogOkE) {
calogAggFree(agg);
return status;
}
status = calogAggPush(agg, &element);
if (status != calogOkE) {
calogValueFree(&element);
calogAggFree(agg);
return status;
}
jsonSkipWs(p);
if (p->cur >= p->end) {
calogAggFree(agg);
return calogErrArgE;
}
if (*p->cur == ',') {
p->cur++;
continue;
}
if (*p->cur == ']') {
p->cur++;
break;
}
calogAggFree(agg);
return calogErrArgE;
}
calogValueAgg(out, agg);
return calogOkE;
}
static int32_t jsonParseLiteral(JsonParseT *p, CalogValueT *out) {
size_t remaining;
calogValueNil(out);
remaining = (size_t)(p->end - p->cur);
if (remaining >= 4 && memcmp(p->cur, "true", 4) == 0) {
calogValueBool(out, true);
p->cur += 4;
return calogOkE;
}
if (remaining >= 5 && memcmp(p->cur, "false", 5) == 0) {
calogValueBool(out, false);
p->cur += 5;
return calogOkE;
}
if (remaining >= 4 && memcmp(p->cur, "null", 4) == 0) {
calogValueNil(out);
p->cur += 4;
return calogOkE;
}
return calogErrArgE;
}
static int32_t jsonParseNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
JsonParseT parse;
int32_t status;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "jsonParse expects (text)");
}
parse.cur = args[0].as.s.bytes;
parse.end = args[0].as.s.bytes + args[0].as.s.length;
status = jsonParseValue(&parse, result, 0);
if (status != calogOkE) {
calogValueFree(result);
return calogFail(result, status, "jsonParse: invalid JSON");
}
jsonSkipWs(&parse);
if (parse.cur != parse.end) {
calogValueFree(result);
return calogFail(result, calogErrArgE, "jsonParse: trailing characters after JSON value");
}
return calogOkE;
}
static int32_t jsonParseNumber(JsonParseT *p, CalogValueT *out) {
const char *start;
char *stop;
bool isReal;
calogValueNil(out);
start = p->cur;
isReal = false;
if (p->cur < p->end && *p->cur == '-') {
p->cur++;
}
while (p->cur < p->end) {
char c;
c = *p->cur;
if (c >= '0' && c <= '9') {
p->cur++;
} else if (c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-') {
isReal = true;
p->cur++;
} else {
break;
}
}
if (p->cur == start) {
return calogErrArgE;
}
if (isReal) {
double r;
r = strtod(start, &stop);
if (stop != p->cur) {
return calogErrArgE;
}
calogValueReal(out, r);
return calogOkE;
}
{
long long i;
errno = 0;
i = strtoll(start, &stop, 10);
if (stop != p->cur) {
return calogErrArgE;
}
if (errno == ERANGE) {
// An integer literal that overflows int64 becomes a real, rather than silently
// saturating to LLONG_MAX/MIN.
double r;
r = strtod(start, &stop);
if (stop != p->cur) {
return calogErrArgE;
}
calogValueReal(out, r);
return calogOkE;
}
calogValueInt(out, (int64_t)i);
return calogOkE;
}
}
static int32_t jsonParseObject(JsonParseT *p, CalogValueT *out, int32_t depth) {
CalogAggT *agg;
int32_t status;
calogValueNil(out);
status = calogAggCreate(&agg, calogMapE);
if (status != calogOkE) {
return status;
}
p->cur++; // consume '{'
jsonSkipWs(p);
if (p->cur < p->end && *p->cur == '}') {
p->cur++;
calogValueAgg(out, agg);
return calogOkE;
}
for (;;) {
CalogValueT key;
CalogValueT value;
jsonSkipWs(p);
if (p->cur >= p->end || *p->cur != '"') {
calogAggFree(agg);
return calogErrArgE;
}
status = jsonParseString(p, &key);
if (status != calogOkE) {
calogAggFree(agg);
return status;
}
jsonSkipWs(p);
if (p->cur >= p->end || *p->cur != ':') {
calogValueFree(&key);
calogAggFree(agg);
return calogErrArgE;
}
p->cur++; // consume ':'
status = jsonParseValue(p, &value, depth + 1);
if (status != calogOkE) {
calogValueFree(&key);
calogAggFree(agg);
return status;
}
status = calogAggSet(agg, &key, &value);
if (status != calogOkE) {
calogValueFree(&key);
calogValueFree(&value);
calogAggFree(agg);
return status;
}
jsonSkipWs(p);
if (p->cur >= p->end) {
calogAggFree(agg);
return calogErrArgE;
}
if (*p->cur == ',') {
p->cur++;
continue;
}
if (*p->cur == '}') {
p->cur++;
break;
}
calogAggFree(agg);
return calogErrArgE;
}
calogValueAgg(out, agg);
return calogOkE;
}
static int32_t jsonParseString(JsonParseT *p, CalogValueT *out) {
JsonBufT buf;
int32_t status;
calogValueNil(out);
buf.bytes = NULL;
buf.length = 0;
buf.cap = 0;
p->cur++; // consume opening '"'
while (p->cur < p->end) {
unsigned char c;
c = (unsigned char)*p->cur;
if (c == '"') {
p->cur++;
status = calogValueString(out, (buf.bytes != NULL) ? buf.bytes : "", (int64_t)buf.length);
jsonBufFree(&buf);
return status;
}
if (c == '\\') {
char e;
p->cur++;
if (p->cur >= p->end) {
jsonBufFree(&buf);
return calogErrArgE;
}
e = *p->cur;
switch (e) {
case '"':
status = jsonBufPutChar(&buf, '"');
break;
case '\\':
status = jsonBufPutChar(&buf, '\\');
break;
case '/':
status = jsonBufPutChar(&buf, '/');
break;
case 'b':
status = jsonBufPutChar(&buf, '\b');
break;
case 'f':
status = jsonBufPutChar(&buf, '\f');
break;
case 'n':
status = jsonBufPutChar(&buf, '\n');
break;
case 'r':
status = jsonBufPutChar(&buf, '\r');
break;
case 't':
status = jsonBufPutChar(&buf, '\t');
break;
case 'u':
status = jsonParseUnicode(p, &buf);
break;
default:
jsonBufFree(&buf);
return calogErrArgE;
}
if (status != calogOkE) {
jsonBufFree(&buf);
return status;
}
if (e != 'u') {
p->cur++;
}
continue;
}
if (c < 0x20) {
jsonBufFree(&buf);
return calogErrArgE;
}
status = jsonBufPutChar(&buf, (char)c);
if (status != calogOkE) {
jsonBufFree(&buf);
return status;
}
p->cur++;
}
jsonBufFree(&buf);
return calogErrArgE;
}
static int32_t jsonParseUnicode(JsonParseT *p, JsonBufT *buf) {
uint32_t cp;
int32_t status;
p->cur++; // consume 'u'
status = jsonHexQuad(p, &cp);
if (status != calogOkE) {
return status;
}
if (cp >= 0xD800 && cp <= 0xDBFF) {
uint32_t lo;
if (p->cur + 1 >= p->end || p->cur[0] != '\\' || p->cur[1] != 'u') {
return calogErrArgE;
}
p->cur += 2; // consume "\u"
status = jsonHexQuad(p, &lo);
if (status != calogOkE) {
return status;
}
if (lo < 0xDC00 || lo > 0xDFFF) {
return calogErrArgE;
}
cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00);
} else if (cp >= 0xDC00 && cp <= 0xDFFF) {
return calogErrArgE; // an unpaired low surrogate is not a valid code point
}
return jsonPutUtf8(buf, cp);
}
static int32_t jsonParseValue(JsonParseT *p, CalogValueT *out, int32_t depth) {
calogValueNil(out);
if (depth >= CALOG_MAX_DEPTH) {
return calogErrDepthE;
}
jsonSkipWs(p);
if (p->cur >= p->end) {
return calogErrArgE;
}
switch (*p->cur) {
case '{':
return jsonParseObject(p, out, depth);
case '[':
return jsonParseArray(p, out, depth);
case '"':
return jsonParseString(p, out);
case 't':
case 'f':
case 'n':
return jsonParseLiteral(p, out);
default:
return jsonParseNumber(p, out);
}
}
static int32_t jsonPutUtf8(JsonBufT *buf, uint32_t cp) {
char bytes[4];
size_t count;
if (cp < 0x80) {
bytes[0] = (char)cp;
count = 1;
} else if (cp < 0x800) {
bytes[0] = (char)(0xC0 | (cp >> 6));
bytes[1] = (char)(0x80 | (cp & 0x3F));
count = 2;
} else if (cp < 0x10000) {
bytes[0] = (char)(0xE0 | (cp >> 12));
bytes[1] = (char)(0x80 | ((cp >> 6) & 0x3F));
bytes[2] = (char)(0x80 | (cp & 0x3F));
count = 3;
} else {
bytes[0] = (char)(0xF0 | (cp >> 18));
bytes[1] = (char)(0x80 | ((cp >> 12) & 0x3F));
bytes[2] = (char)(0x80 | ((cp >> 6) & 0x3F));
bytes[3] = (char)(0x80 | (cp & 0x3F));
count = 4;
}
return jsonBufPutBytes(buf, bytes, count);
}
static void jsonSkipWs(JsonParseT *p) {
while (p->cur < p->end) {
char c;
c = *p->cur;
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
p->cur++;
} else {
break;
}
}
}
static int32_t jsonStringifyNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
JsonBufT buf;
int32_t status;
(void)userData;
calogValueNil(result);
if (argCount != 1) {
return calogFail(result, calogErrArgE, "jsonStringify expects (value)");
}
buf.bytes = NULL;
buf.length = 0;
buf.cap = 0;
status = jsonEncode(&buf, &args[0], 0);
if (status != calogOkE) {
jsonBufFree(&buf);
return calogFail(result, status, "jsonStringify: value is not JSON-serializable");
}
status = calogValueString(result, (buf.bytes != NULL) ? buf.bytes : "", (int64_t)buf.length);
jsonBufFree(&buf);
return status;
}