// 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 #include #include #include #include #include #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; }