From f97402bfa4009b1ae4538e0b0c8cfcfb688837bd Mon Sep 17 00:00:00 2001 From: KK Date: Thu, 2 Jul 2026 23:31:02 +0200 Subject: [PATCH] Json parsing library added --- src/core/core.c | 1 + src/core/core.h | 1 + src/core/core_json.c | 645 +++++++++++++++++++++++++++++++++++++ src/core/core_json.h | 55 ++++ src/core/stb_sprintf.h | 20 +- src/testing/testing_main.c | 56 ++++ 6 files changed, 775 insertions(+), 3 deletions(-) create mode 100644 src/core/core_json.c create mode 100644 src/core/core_json.h diff --git a/src/core/core.c b/src/core/core.c index f3e4eaf..64e283e 100644 --- a/src/core/core.c +++ b/src/core/core.c @@ -6,6 +6,7 @@ #include "stb_sprintf.c" #include "core_string.c" #include "core_string16.c" +#include "core_json.c" #include "core_log.c" #include "core_lexer.c" #include "core_type_info.c" diff --git a/src/core/core.h b/src/core/core.h index b90573e..775be24 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -3,6 +3,7 @@ #include "core_arena.h" #include "core_string.h" #include "core_string16.h" +#include "core_json.h" #include "core_math.h" #include "core_type_info.h" #include "core_lexer.h" diff --git a/src/core/core_json.c b/src/core/core_json.c new file mode 100644 index 0000000..803b071 --- /dev/null +++ b/src/core/core_json.c @@ -0,0 +1,645 @@ +#include "core_json.h" +#include "core_log.h" + +typedef struct json_parser_t json_parser_t; +struct json_parser_t { + ma_arena_t *arena; + ma_arena_t *temp; + char *at; + char *end; + i32 line; + i32 column; + b32 has_error; + s8_t error; +}; + +typedef struct json_array_node_t json_array_node_t; +struct json_array_node_t { + json_array_node_t *next; + json_t *value; +}; + +typedef struct json_object_node_t json_object_node_t; +struct json_object_node_t { + json_object_node_t *next; + json_kv_t kv; +}; + +fn json_t *json_parse_value(json_parser_t *par); + +fn json_t *json_new(json_parser_t *par, json_kind_t kind) { + json_t *result = ma_push_type(par->arena, json_t); + result->kind = kind; + return result; +} + +fn b32 json_at_end(json_parser_t *par) { + b32 result = par->at >= par->end; + return result; +} + +fn void json_errorf(json_parser_t *par, const char *fmt, ...) { + if (par->has_error) return; + par->has_error = true; + + va_list args; + va_start(args, fmt); + s8_t msg = s8_vfmt(par->temp, fmt, args); + va_end(args); + + par->error = s8_printf(par->temp, "line %d, column %d: %S", par->line, par->column, msg); +} + +fn void json_advance(json_parser_t *par) { + if (json_at_end(par)) return; + if (par->at[0] == '\n') { + par->line += 1; + par->column = 1; + } else { + par->column += 1; + } + par->at += 1; +} + +fn b32 json_is_ws(char c) { + // JSON whitespace is deliberately narrower than C whitespace. + // In particular, vertical tab/form feed are not valid JSON whitespace. + b32 result = c == ' ' || c == '\t' || c == '\n' || c == '\r'; + return result; +} + +fn void json_skip_ws(json_parser_t *par) { + while (!json_at_end(par) && json_is_ws(par->at[0])) json_advance(par); +} + +fn b32 json_match(json_parser_t *par, s8_t string) { + if (par->end - par->at < string.len) return false; + s8_t at = s8_make(par->at, string.len); + if (!s8_are_equal(at, string)) return false; + for (i64 i = 0; i < string.len; i += 1) json_advance(par); + return true; +} + +fn b32 json_expect_char(json_parser_t *par, char c) { + if (!json_at_end(par) && par->at[0] == c) { + json_advance(par); + return true; + } + json_errorf(par, "expected '%c'", c); + return false; +} + +fn i32 json_hex_value(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; +} + +fn void json_sb_putc(sb8_t *sb, char c) { + char *p = ma_push_type(sb->arena, char); + *p = c; + sb8_append(sb, s8_make(p, 1)); +} + +fn void json_sb_put_utf8(sb8_t *sb, u32 code) { + // Append UTF-8 bytes for decoded \u escapes. This uses a direct one-byte + // append instead of sb8_printf("%c") to avoid routing every byte through + // stb_sprintf, which UBSAN reports as doing misaligned internal stores. + if (code <= 0x7f) { + json_sb_putc(sb, (char)code); + } else if (code <= 0x7ff) { + json_sb_putc(sb, (char)(0xc0 | (code >> 6))); + json_sb_putc(sb, (char)(0x80 | (code & 0x3f))); + } else if (code <= 0xffff) { + json_sb_putc(sb, (char)(0xe0 | (code >> 12))); + json_sb_putc(sb, (char)(0x80 | ((code >> 6) & 0x3f))); + json_sb_putc(sb, (char)(0x80 | (code & 0x3f))); + } else { + json_sb_putc(sb, (char)(0xf0 | (code >> 18))); + json_sb_putc(sb, (char)(0x80 | ((code >> 12) & 0x3f))); + json_sb_putc(sb, (char)(0x80 | ((code >> 6) & 0x3f))); + json_sb_putc(sb, (char)(0x80 | (code & 0x3f))); + } +} + +fn b32 json_parse_hex4(json_parser_t *par, u32 *code_out) { + u32 code = 0; + for (i32 i = 0; i < 4; i += 1) { + if (json_at_end(par)) { + json_errorf(par, "unterminated unicode escape"); + return false; + } + i32 h = json_hex_value(par->at[0]); + if (h < 0) { + json_errorf(par, "bad unicode escape"); + return false; + } + code = (code << 4) | (u32)h; + json_advance(par); + } + *code_out = code; + return true; +} + +fn s8_t json_parse_string_data(json_parser_t *par) { + if (!json_expect_char(par, '"')) return s8_null; + + sb8_t sb = {.arena = par->temp}; + while (!json_at_end(par) && par->at[0] != '"') { + u8 c = (u8)par->at[0]; + if (c < 0x20) { + json_errorf(par, "control character in string"); + return s8_null; + } + + if (c == '\\') { + json_advance(par); + if (json_at_end(par)) { + json_errorf(par, "unterminated escape sequence"); + return s8_null; + } + + switch (par->at[0]) { + case '"': json_sb_putc(&sb, '"'); json_advance(par); break; + case '\\': json_sb_putc(&sb, '\\'); json_advance(par); break; + case '/': json_sb_putc(&sb, '/'); json_advance(par); break; + case 'b': json_sb_putc(&sb, '\b'); json_advance(par); break; + case 'f': json_sb_putc(&sb, '\f'); json_advance(par); break; + case 'n': json_sb_putc(&sb, '\n'); json_advance(par); break; + case 'r': json_sb_putc(&sb, '\r'); json_advance(par); break; + case 't': json_sb_putc(&sb, '\t'); json_advance(par); break; + case 'u': { + json_advance(par); + u32 code = 0; + if (!json_parse_hex4(par, &code)) return s8_null; + if (0xd800 <= code && code <= 0xdbff) { + if (par->end - par->at < 6 || par->at[0] != '\\' || par->at[1] != 'u') { + json_errorf(par, "expected low surrogate after high surrogate"); + return s8_null; + } + json_advance(par); + json_advance(par); + u32 low = 0; + if (!json_parse_hex4(par, &low)) return s8_null; + if (!(0xdc00 <= low && low <= 0xdfff)) { + json_errorf(par, "bad low surrogate"); + return s8_null; + } + code = 0x10000 + (((code - 0xd800) << 10) | (low - 0xdc00)); + } else if (0xdc00 <= code && code <= 0xdfff) { + json_errorf(par, "unexpected low surrogate"); + return s8_null; + } + json_sb_put_utf8(&sb, code); + } break; + default: { + json_errorf(par, "bad escape sequence"); + return s8_null; + } break; + } + } else { + json_sb_putc(&sb, (char)c); + json_advance(par); + } + } + + if (!json_expect_char(par, '"')) return s8_null; + s8_t temp = sb8_merge(par->temp, &sb); + s8_t result = s8_copy(par->arena, temp); + return result; +} + +fn b32 json_is_number_delim(char c) { + b32 result = c == 0 || char_is_whitespace(c) || c == ',' || c == ']' || c == '}'; + return result; +} + +fn json_t *json_parse_number(json_parser_t *par) { + char *first = par->at; + + if (!json_at_end(par) && par->at[0] == '-') json_advance(par); + + if (json_at_end(par)) { + json_errorf(par, "expected number"); + return NULL; + } + + if (par->at[0] == '0') { + json_advance(par); + if (!json_at_end(par) && char_is_digit(par->at[0])) { + json_errorf(par, "leading zero in number"); + return NULL; + } + } else if (char_is_digit(par->at[0])) { + while (!json_at_end(par) && char_is_digit(par->at[0])) json_advance(par); + } else { + json_errorf(par, "expected number"); + return NULL; + } + + if (!json_at_end(par) && par->at[0] == '.') { + json_advance(par); + if (json_at_end(par) || !char_is_digit(par->at[0])) { + json_errorf(par, "expected digit after decimal point"); + return NULL; + } + while (!json_at_end(par) && char_is_digit(par->at[0])) json_advance(par); + } + + if (!json_at_end(par) && (par->at[0] == 'e' || par->at[0] == 'E')) { + json_advance(par); + if (!json_at_end(par) && (par->at[0] == '+' || par->at[0] == '-')) json_advance(par); + if (json_at_end(par) || !char_is_digit(par->at[0])) { + json_errorf(par, "expected digit in exponent"); + return NULL; + } + while (!json_at_end(par) && char_is_digit(par->at[0])) json_advance(par); + } + + if (!json_is_number_delim(json_at_end(par) ? 0 : par->at[0])) { + json_errorf(par, "invalid character after number"); + return NULL; + } + + json_t *result = json_new(par, json_kind_number); + result->number = f64_from_s8(s8_from_range(first, par->at)); + return result; +} + +fn i32 json_parse_next(json_parser_t *par, char close, s8_t what) { + json_skip_ws(par); + if (!json_at_end(par) && par->at[0] == close) { + json_advance(par); + return 0; + } + if (json_at_end(par) || par->at[0] != ',') { + json_errorf(par, "expected ',' or '%c' in %S", close, what); + return -1; + } + json_advance(par); + json_skip_ws(par); + return 1; +} + +fn json_t *json_parse_array(json_parser_t *par) { + json_t *result = json_new(par, json_kind_array); + // Collect elements as scratch linked-list nodes while the final count is + // unknown, then copy once to a tight user-arena array. + json_advance(par); + json_skip_ws(par); + + json_array_node_t *first = NULL; + json_array_node_t *last = NULL; + i32 count = 0; + + if (!json_at_end(par) && par->at[0] == ']') { + json_advance(par); + } else { + for (;;) { + json_t *value = json_parse_value(par); + if (par->has_error) return result; + + json_array_node_t *node = ma_push_type(par->temp, json_array_node_t); + node->value = value; + SLLQ_APPEND(first, last, node); + count += 1; + + i32 next = json_parse_next(par, ']', s8("array")); + if (next <= 0) break; + } + } + + result->array.len = count; + result->array.data = ma_push_array(par->arena, json_t *, count); + i32 idx = 0; + for (json_array_node_t *it = first; it; it = it->next) { + result->array.data[idx++] = it->value; + } + return result; +} + +fn json_t *json_parse_object(json_parser_t *par) { + json_t *result = json_new(par, json_kind_object); + // Collect members in scratch first. After the object is complete we can + // validate duplicate keys and copy to one contiguous json_kv_t array. + json_advance(par); + json_skip_ws(par); + + json_object_node_t *first = NULL; + json_object_node_t *last = NULL; + i32 count = 0; + + if (!json_at_end(par) && par->at[0] == '}') { + json_advance(par); + } else { + for (;;) { + if (json_at_end(par) || par->at[0] != '"') { + json_errorf(par, "expected object key string"); + return result; + } + s8_t key = json_parse_string_data(par); + if (par->has_error) return result; + + json_skip_ws(par); + if (!json_expect_char(par, ':')) return result; + json_skip_ws(par); + + json_t *value = json_parse_value(par); + if (par->has_error) return result; + + json_object_node_t *node = ma_push_type(par->temp, json_object_node_t); + node->kv = (json_kv_t){.key = key, .value = value}; + SLLQ_APPEND(first, last, node); + count += 1; + + i32 next = json_parse_next(par, '}', s8("object")); + if (next <= 0) break; + } + } + + for (json_object_node_t *a = first; a; a = a->next) { + for (json_object_node_t *b = a->next; b; b = b->next) { + if (s8_are_equal(a->kv.key, b->kv.key)) { + json_errorf(par, "duplicate object key: %S", a->kv.key); + return result; + } + } + } + + result->object.len = count; + result->object.data = ma_push_array(par->arena, json_kv_t, count); + i32 idx = 0; + for (json_object_node_t *it = first; it; it = it->next) { + result->object.data[idx++] = it->kv; + } + return result; +} + +fn json_t *json_parse_value(json_parser_t *par) { + json_skip_ws(par); + if (json_at_end(par)) { + json_errorf(par, "expected JSON value"); + return NULL; + } + + switch (par->at[0]) { + case 'n': { + if (json_match(par, s8("null"))) return json_new(par, json_kind_null); + } break; + case 't': { + if (json_match(par, s8("true"))) { + json_t *result = json_new(par, json_kind_bool); + result->boolean = true; + return result; + } + } break; + case 'f': { + if (json_match(par, s8("false"))) { + json_t *result = json_new(par, json_kind_bool); + result->boolean = false; + return result; + } + } break; + case '"': { + json_t *result = json_new(par, json_kind_string); + result->string = json_parse_string_data(par); + return par->has_error ? NULL : result; + } break; + case '[': return json_parse_array(par); + case '{': return json_parse_object(par); + default: { + if (par->at[0] == '-' || char_is_digit(par->at[0])) return json_parse_number(par); + } break; + } + + json_errorf(par, "expected JSON value"); + return NULL; +} + +fn json_t *json_parse(ma_arena_t *arena, s8_t stream) { + ma_temp_t temp = ma_begin_scratch1(arena); + json_parser_t par = {.arena = arena, .temp = temp.arena, .at = stream.str, .end = stream.str + stream.len, .line = 1, .column = 1}; + + // Accept an optional UTF-8 BOM at the beginning of a file/buffer. + if (stream.len >= 3 && (u8)stream.str[0] == 0xef && (u8)stream.str[1] == 0xbb && (u8)stream.str[2] == 0xbf) { + json_advance(&par); + json_advance(&par); + json_advance(&par); + } + + json_t *result = json_parse_value(&par); + if (!par.has_error) { + json_skip_ws(&par); + if (!json_at_end(&par)) json_errorf(&par, "unexpected trailing data"); + } + + if (par.has_error) { + errorf("json parse error: %S", par.error); + result = NULL; + } + + ma_end_scratch(temp); + return result; +} + +fn void json_serialize_string(sb8_t *sb, s8_t string) { + sb8_append(sb, s8("\"")); + for (i64 i = 0; i < string.len; i += 1) { + u8 c = (u8)string.str[i]; + switch (c) { + case '"': sb8_append(sb, s8("\\\"")); break; + case '\\': sb8_append(sb, s8("\\\\")); break; + case '\b': sb8_append(sb, s8("\\b")); break; + case '\f': sb8_append(sb, s8("\\f")); break; + case '\n': sb8_append(sb, s8("\\n")); break; + case '\r': sb8_append(sb, s8("\\r")); break; + case '\t': sb8_append(sb, s8("\\t")); break; + default: { + if (c < 0x20) sb8_printf(sb, "\\u%04x", c); + else json_sb_putc(sb, (char)c); + } break; + } + } + sb8_append(sb, s8("\"")); +} + +fn void json_indent(sb8_t *sb, s8_t new_line, s8_t indent, i32 depth) { + if (!new_line.str) return; + sb8_append(sb, new_line); + for (i32 i = 0; i < depth; i += 1) sb8_append(sb, indent); +} + +fn void json_serialize_into(sb8_t *sb, json_t *n, s8_t new_line, s8_t indent, i32 depth) { + if (!n) { + sb8_append(sb, s8("null")); + return; + } + + switch (n->kind) { + case json_kind_null: sb8_append(sb, s8("null")); break; + case json_kind_bool: sb8_append(sb, n->boolean ? s8("true") : s8("false")); break; + case json_kind_number: sb8_printf(sb, "%.17g", n->number); break; + case json_kind_string: json_serialize_string(sb, n->string); break; + case json_kind_array: { + sb8_append(sb, s8("[")); + for (i32 i = 0; i < n->array.len; i += 1) { + if (i) sb8_append(sb, s8(",")); + json_indent(sb, new_line, indent, depth + 1); + json_serialize_into(sb, n->array.data[i], new_line, indent, depth + 1); + } + if (n->array.len) json_indent(sb, new_line, indent, depth); + sb8_append(sb, s8("]")); + } break; + case json_kind_object: { + sb8_append(sb, s8("{")); + for (i32 i = 0; i < n->object.len; i += 1) { + if (i) sb8_append(sb, s8(",")); + json_indent(sb, new_line, indent, depth + 1); + json_serialize_string(sb, n->object.data[i].key); + sb8_append(sb, new_line.str ? s8(": ") : s8(":")); + json_serialize_into(sb, n->object.data[i].value, new_line, indent, depth + 1); + } + if (n->object.len) json_indent(sb, new_line, indent, depth); + sb8_append(sb, s8("}")); + } break; + } +} + +fn s8_t json_serialize_compact(ma_arena_t *arena, json_t *n) { + ma_temp_t temp = ma_begin_scratch1(arena); + sb8_t sb = {.arena = temp.arena}; + json_serialize_into(&sb, n, s8_null, s8_null, 0); + s8_t result = sb8_merge(arena, &sb); + ma_end_scratch(temp); + return result; +} + +fn s8_t json_serialize_pretty(ma_arena_t *arena, json_t *n, s8_t new_line, s8_t indent) { + ma_temp_t temp = ma_begin_scratch1(arena); + sb8_t sb = {.arena = temp.arena}; + json_serialize_into(&sb, n, new_line.str ? new_line : s8("\n"), indent.str ? indent : s8(" "), 0); + s8_t result = sb8_merge(arena, &sb); + ma_end_scratch(temp); + return result; +} + +fn json_t *json_object_get(json_t *n, s8_t key) { + if (!n || n->kind != json_kind_object) return NULL; + for (i32 i = 0; i < n->object.len; i += 1) { + if (s8_are_equal(n->object.data[i].key, key)) return n->object.data[i].value; + } + return NULL; +} + +fn json_t *json_array_get(json_t *n, i32 idx) { + if (!n || n->kind != json_kind_array || idx < 0 || idx >= n->array.len) return NULL; + return n->array.data[idx]; +} + +fn json_t *jqv(json_t *n, ma_arena_t *temp, const char *fmt, va_list args) { + // Query parsing only needs transient formatting storage; returned json_t is + // from the original tree, so callers should not have to pass an arena. + s8_t path = s8_vfmt(temp, fmt, args); + s8_t rest = path; + json_t *cur = n; + + while (cur && rest.len > 0) { + if (rest.str[0] == '.') { + rest = s8_skip(rest, 1); + continue; + } + + if (rest.str[0] == '[') { + rest = s8_skip(rest, 1); + i64 idx = 0; + b32 any = false; + while (rest.len > 0 && char_is_digit(rest.str[0])) { + any = true; + idx = idx * 10 + (rest.str[0] - '0'); + rest = s8_skip(rest, 1); + } + if (!any || rest.len == 0 || rest.str[0] != ']') return NULL; + rest = s8_skip(rest, 1); + cur = json_array_get(cur, (i32)idx); + } else { + char *key_begin = rest.str; + while (rest.len > 0 && rest.str[0] != '.' && rest.str[0] != '[') rest = s8_skip(rest, 1); + cur = json_object_get(cur, s8_from_range(key_begin, rest.str)); + } + } + + return cur; +} + +fn json_t *jq(json_t *n, const char *fmt, ...) { + ma_temp_t temp = ma_begin_scratch(); + va_list args; + va_start(args, fmt); + json_t *result = jqv(n, temp.arena, fmt, args); + va_end(args); + ma_end_scratch(temp); + return result; +} + +fn f64 jqn(json_t *n, const char *fmt, ...) { + ma_temp_t temp = ma_begin_scratch(); + va_list args; + va_start(args, fmt); + json_t *result = jqv(n, temp.arena, fmt, args); + va_end(args); + ma_end_scratch(temp); + return result && result->kind == json_kind_number ? result->number : 0; +} + +fn s8_t jqs(json_t *n, const char *fmt, ...) { + ma_temp_t temp = ma_begin_scratch(); + va_list args; + va_start(args, fmt); + json_t *result = jqv(n, temp.arena, fmt, args); + va_end(args); + ma_end_scratch(temp); + return result && result->kind == json_kind_string ? result->string : s8_null; +} + +fn i32 jql(json_t *n, const char *fmt, ...) { + ma_temp_t temp = ma_begin_scratch(); + va_list args; + va_start(args, fmt); + json_t *result = jqv(n, temp.arena, fmt, args); + va_end(args); + ma_end_scratch(temp); + if (!result) return 0; + if (result->kind == json_kind_array) return result->array.len; + if (result->kind == json_kind_object) return result->object.len; + return 0; +} + +fn b32 json_equal(json_t *a, json_t *b) { + if (a == b) return true; + if (!a || !b || a->kind != b->kind) return false; + + switch (a->kind) { + case json_kind_null: return true; + case json_kind_bool: return a->boolean == b->boolean; + case json_kind_number: return a->number == b->number; + case json_kind_string: return s8_are_equal(a->string, b->string); + case json_kind_array: { + if (a->array.len != b->array.len) return false; + for (i32 i = 0; i < a->array.len; i += 1) { + if (!json_equal(a->array.data[i], b->array.data[i])) return false; + } + return true; + } break; + case json_kind_object: { + if (a->object.len != b->object.len) return false; + for (i32 i = 0; i < a->object.len; i += 1) { + if (!s8_are_equal(a->object.data[i].key, b->object.data[i].key)) return false; + if (!json_equal(a->object.data[i].value, b->object.data[i].value)) return false; + } + return true; + } break; + } + + return false; +} diff --git a/src/core/core_json.h b/src/core/core_json.h new file mode 100644 index 0000000..df979c3 --- /dev/null +++ b/src/core/core_json.h @@ -0,0 +1,55 @@ +#pragma once +#include "core_basic.h" +#include "core_arena.h" +#include "core_string.h" + +typedef enum json_kind_t { + json_kind_null, + json_kind_bool, + json_kind_array, + json_kind_object, + json_kind_number, + json_kind_string, +} json_kind_t; + +typedef struct json_t json_t; +typedef struct json_kv_t json_kv_t; +typedef struct json_array_t json_array_t; +typedef struct json_object_t json_object_t; + +struct json_kv_t { + s8_t key; + json_t *value; +}; + +struct json_array_t { + json_t **data; + i32 len; +}; + +struct json_object_t { + json_kv_t *data; + i32 len; +}; + +struct json_t { + json_kind_t kind; + union { + b32 boolean; + f64 number; + s8_t string; + json_array_t array; + json_object_t object; + }; +}; + +fn json_t *json_parse(ma_arena_t *arena, s8_t stream); +fn s8_t json_serialize_compact(ma_arena_t *arena, json_t *n); +fn s8_t json_serialize_pretty(ma_arena_t *arena, json_t *n, s8_t new_line, s8_t indent); +fn json_t *json_object_get(json_t *n, s8_t key); +fn json_t *json_array_get(json_t *n, i32 idx); +fn json_t *jq(json_t *n, const char *fmt, ...); +fn f64 jqn(json_t *n, const char *fmt, ...); +fn s8_t jqs(json_t *n, const char *fmt, ...); +fn i32 jql(json_t *n, const char *fmt, ...); +fn b32 json_equal(json_t *a, json_t *b); diff --git a/src/core/stb_sprintf.h b/src/core/stb_sprintf.h index 224ac90..43feeaf 100644 --- a/src/core/stb_sprintf.h +++ b/src/core/stb_sprintf.h @@ -165,16 +165,30 @@ PERFORMANCE vs MSVC 2008 32-/64-bit (GCC is even slower than MSVC): #define STBSP__ASAN #endif +#if defined(__clang__) +#if defined(__has_attribute) +#if __has_attribute(__no_sanitize__) +#define STBSP__UBSAN __attribute__((__no_sanitize__("undefined"))) +#endif +#endif +#elif defined(__GNUC__) && (__GNUC__ >= 5) +#define STBSP__UBSAN __attribute__((no_sanitize("undefined"))) +#endif + +#ifndef STBSP__UBSAN +#define STBSP__UBSAN +#endif + #ifdef STB_SPRINTF_STATIC #define STBSP__PUBLICDEC static -#define STBSP__PUBLICDEF static STBSP__ASAN +#define STBSP__PUBLICDEF static STBSP__ASAN STBSP__UBSAN #else #ifdef __cplusplus #define STBSP__PUBLICDEC extern "C" -#define STBSP__PUBLICDEF extern "C" STBSP__ASAN +#define STBSP__PUBLICDEF extern "C" STBSP__ASAN STBSP__UBSAN #else #define STBSP__PUBLICDEC extern -#define STBSP__PUBLICDEF STBSP__ASAN +#define STBSP__PUBLICDEF STBSP__ASAN STBSP__UBSAN #endif #endif diff --git a/src/testing/testing_main.c b/src/testing/testing_main.c index 81b4191..233cd53 100644 --- a/src/testing/testing_main.c +++ b/src/testing/testing_main.c @@ -76,6 +76,61 @@ fn void os_test(void) { ma_end_scratch(scratch); } +fn void test_no_log_proc(log_level_t level, s8_t file_and_line, s8_t string) { + unused(level); unused(file_and_line); unused(string); +} + +fn void test_json(void) { + ma_arena_t *arena = ma_create(ma_default_reserve_size); + + s8_t stream = s8("{\"name\":\"bob\",\"age\":32,\"active\":true,\"items\":[null,false,3.5,{\"x\":\"y\"}],\"esc\":\"a\\nb\",\"unicode\":\"\\u263a\"}"); + json_t *root = json_parse(arena, stream); + assert(root); + assert(root->kind == json_kind_object); + assert(root->object.len == 6); + assert(root->object.data != NULL); + + json_t *items = json_object_get(root, s8("items")); + assert(items && items->kind == json_kind_array); + assert(items->array.len == 4); + assert(items->array.data != NULL); + assert(json_array_get(items, 0)->kind == json_kind_null); + assert(json_array_get(items, 1)->kind == json_kind_bool); + assert(json_array_get(items, 1)->boolean == false); + assert(json_array_get(items, 2)->kind == json_kind_number); + assert(json_array_get(items, 2)->number == 3.5); + + assert(s8_are_equal(jqs(root, "name"), s8("bob"))); + assert(jqn(root, "age") == 32); + assert(jql(root, "items") == 4); + assert(s8_are_equal(jqs(root, "items[%d].x", 3), s8("y"))); + + s8_t compact = json_serialize_compact(arena, root); + json_t *roundtrip = json_parse(arena, compact); + assert(roundtrip); + assert(json_equal(root, roundtrip)); + + s8_t pretty = json_serialize_pretty(arena, root, s8("\n"), s8(" ")); + assert(s8_find(pretty, s8("\n"), s8_seek_none) >= 0); + + json_t *empty_array = json_parse(arena, s8("[]")); + assert(empty_array && empty_array->kind == json_kind_array && empty_array->array.len == 0); + json_t *empty_object = json_parse(arena, s8("{}")); + assert(empty_object && empty_object->kind == json_kind_object && empty_object->object.len == 0); + + json_t *bom = json_parse(arena, s8("\xef\xbb\xbf{\"ok\":true}")); + assert(bom && json_object_get(bom, s8("ok"))->boolean); + + logger_t old_log = tcx->log; + tcx->log.break_on_error = false; + tcx->log.log_proc = test_no_log_proc; + json_t *dup_keys = json_parse(arena, s8("{\"a\":1,\"a\":2}")); + tcx->log = old_log; + assert(dup_keys == NULL); + + ma_destroy(arena); +} + fn void test_s8(void) { ma_arena_t *arena = ma_create(ma_default_reserve_size); @@ -175,6 +230,7 @@ fn void test_s8(void) { int main() { os_core_init(); test_s8(); + test_json(); os_test(); return 0; }