Compare commits

...

4 Commits

Author SHA1 Message Date
KK
0bea393c13 Csv library 2026-07-03 01:40:22 +02:00
KK
68f9136ba5 Remove misc 2026-07-03 01:21:35 +02:00
KK
f97402bfa4 Json parsing library added 2026-07-02 23:31:02 +02:00
KK
cc5db90495 api query script 2026-07-02 22:55:43 +02:00
8 changed files with 1048 additions and 3 deletions

View File

@@ -6,6 +6,8 @@
#include "stb_sprintf.c"
#include "core_string.c"
#include "core_string16.c"
#include "core_json.c"
#include "core_csv.c"
#include "core_log.c"
#include "core_lexer.c"
#include "core_type_info.c"

View File

@@ -3,6 +3,8 @@
#include "core_arena.h"
#include "core_string.h"
#include "core_string16.h"
#include "core_json.h"
#include "core_csv.h"
#include "core_math.h"
#include "core_type_info.h"
#include "core_lexer.h"

180
src/core/core_csv.c Normal file
View File

@@ -0,0 +1,180 @@
#include "core_csv.h"
#include "core_log.h"
typedef struct csv_field_node_t csv_field_node_t;
struct csv_field_node_t {
csv_field_node_t *next;
s8_t field;
};
fn b32 csv_at_end(csv_parser_t *it) {
b32 result = it->at >= it->end;
return result;
}
fn b32 csv_starts_with(csv_parser_t *it, s8_t string) {
if (string.len == 0) return false;
if (it->end - it->at < string.len) return false;
b32 result = s8_are_equal(s8_make(it->at, string.len), string);
return result;
}
fn void csv_consume(csv_parser_t *it, i64 len) {
it->at += len;
}
fn i64 csv_line_end_len(csv_parser_t *it) {
i64 result = 0;
if (it->line_terminator.len) {
if (csv_starts_with(it, it->line_terminator)) result = it->line_terminator.len;
} else if (!csv_at_end(it)) {
if (it->at[0] == '\r' && it->end - it->at >= 2 && it->at[1] == '\n') result = 2;
else if (it->at[0] == '\n') result = 1;
}
return result;
}
fn void csv_errorf(csv_parser_t *it, const char *fmt, ...) {
if (it->has_error) return;
it->has_error = true;
it->is_valid = false;
va_list args;
va_start(args, fmt);
s8_t msg = s8_vfmt(it->arena, fmt, args);
va_end(args);
it->error = s8_printf(it->arena, "csv row %d: %S", it->row, msg);
errorf("%S", it->error);
}
fn void csv_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 s8_t csv_parse_field(csv_parser_t *it) {
if (!csv_at_end(it) && it->at[0] == '"') {
// Quoted field. The outer quotes are removed and doubled quotes are
// unescaped. Quoted fields may contain delimiters and line endings.
csv_consume(it, 1);
sb8_t sb = {.arena = it->arena};
for (;;) {
if (csv_at_end(it)) {
csv_errorf(it, "unterminated quoted field");
return s8_null;
}
if (it->at[0] == '"') {
csv_consume(it, 1);
if (!csv_at_end(it) && it->at[0] == '"') {
csv_sb_putc(&sb, '"');
csv_consume(it, 1);
continue;
}
break;
}
csv_sb_putc(&sb, it->at[0]);
csv_consume(it, 1);
}
s8_t result = sb8_merge(it->arena, &sb);
return result;
}
// Unquoted field is a view into the input buffer.
char *first = it->at;
while (!csv_at_end(it) && !csv_starts_with(it, it->delimiter) && csv_line_end_len(it) == 0) {
csv_consume(it, 1);
}
s8_t result = s8_from_range(first, it->at);
return result;
}
fn void csv_clear_row(csv_parser_t *it) {
if (it->has_row_temp) {
ma_end_temp(it->row_temp);
it->has_row_temp = false;
}
it->data = NULL;
it->len = 0;
}
fn csv_parser_t csv_iter(ma_arena_t *arena, s8_t stream, csv_params_t params) {
assert(arena);
csv_parser_t result = {0};
result.arena = arena;
result.at = stream.str;
result.end = stream.str + stream.len;
result.delimiter = params.delimiter.len ? params.delimiter : s8(",");
result.line_terminator = params.line_terminator;
result.row = 0;
assert(result.delimiter.len > 0);
csv_advance(&result);
return result;
}
fn b32 csv_is_valid(csv_parser_t *it) {
b32 result = it->is_valid && !it->has_error;
return result;
}
fn void csv_advance(csv_parser_t *it) {
csv_clear_row(it);
if (it->has_error || csv_at_end(it)) {
it->is_valid = false;
return;
}
it->row += 1;
it->row_temp = ma_begin_temp(it->arena);
it->has_row_temp = true;
csv_field_node_t *first = NULL;
csv_field_node_t *last = NULL;
i32 count = 0;
for (;;) {
s8_t field = csv_parse_field(it);
if (it->has_error) return;
csv_field_node_t *node = ma_push_type(it->arena, csv_field_node_t);
node->field = field;
SLLQ_APPEND(first, last, node);
count += 1;
if (csv_starts_with(it, it->delimiter)) {
csv_consume(it, it->delimiter.len);
continue;
}
i64 line_end_len = csv_line_end_len(it);
if (line_end_len) {
csv_consume(it, line_end_len);
break;
}
if (csv_at_end(it)) break;
csv_errorf(it, "expected delimiter or line terminator");
return;
}
// Copy the scratch-like linked list representation into a tight row array.
it->data = ma_push_array(it->arena, s8_t, count);
it->len = count;
i32 idx = 0;
for (csv_field_node_t *node = first; node; node = node->next) {
it->data[idx++] = node->field;
}
it->is_valid = true;
}
fn void csv_end(csv_parser_t *it) {
csv_clear_row(it);
it->is_valid = false;
}

34
src/core/core_csv.h Normal file
View File

@@ -0,0 +1,34 @@
#pragma once
#include "core_basic.h"
#include "core_arena.h"
#include "core_string.h"
typedef struct csv_params_t csv_params_t;
struct csv_params_t {
s8_t delimiter; // defaults to ","
s8_t line_terminator; // defaults to accepting "\n" and "\r\n"
};
typedef struct csv_parser_t csv_parser_t;
struct csv_parser_t {
ma_arena_t *arena;
ma_temp_t row_temp;
b32 has_row_temp;
char *at;
char *end;
s8_t delimiter;
s8_t line_terminator;
s8_t *data;
i32 len;
i32 row;
b32 is_valid;
b32 has_error;
s8_t error;
};
fn csv_parser_t csv_iter(ma_arena_t *arena, s8_t stream, csv_params_t params);
fn b32 csv_is_valid(csv_parser_t *it);
fn void csv_advance(csv_parser_t *it);
fn void csv_end(csv_parser_t *it);

645
src/core/core_json.c Normal file
View File

@@ -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;
}

55
src/core/core_json.h Normal file
View File

@@ -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);

View File

@@ -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

View File

@@ -76,6 +76,117 @@ 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_csv(void) {
ma_arena_t *arena = ma_create(ma_default_reserve_size);
s8_t stream = s8("name,age,note\nBob,32,hello\nAlice,27,\"hi, there\"\n\"Q\"\"uote\"\"\",,tail\n");
csv_params_t params = {0};
i32 row = 0;
for (csv_parser_t it = csv_iter(arena, stream, params); csv_is_valid(&it); csv_advance(&it)) {
if (row == 0) {
assert(it.len == 3);
assert(s8_are_equal(it.data[0], s8("name")));
assert(s8_are_equal(it.data[1], s8("age")));
assert(s8_are_equal(it.data[2], s8("note")));
} else if (row == 1) {
assert(it.len == 3);
assert(s8_are_equal(it.data[0], s8("Bob")));
assert(s8_are_equal(it.data[1], s8("32")));
assert(s8_are_equal(it.data[2], s8("hello")));
} else if (row == 2) {
assert(it.len == 3);
assert(s8_are_equal(it.data[0], s8("Alice")));
assert(s8_are_equal(it.data[1], s8("27")));
assert(s8_are_equal(it.data[2], s8("hi, there")));
} else if (row == 3) {
assert(it.len == 3);
assert(s8_are_equal(it.data[0], s8("Q\"uote\"")));
assert(s8_are_equal(it.data[1], s8("")));
assert(s8_are_equal(it.data[2], s8("tail")));
}
row += 1;
}
assert(row == 4);
s8_t crlf = s8("a,b\r\nc,d\r\n");
row = 0;
for (csv_parser_t it = csv_iter(arena, crlf, params); csv_is_valid(&it); csv_advance(&it)) {
assert(it.len == 2);
if (row == 0) assert(s8_are_equal(it.data[1], s8("b")));
if (row == 1) assert(s8_are_equal(it.data[1], s8("d")));
row += 1;
}
assert(row == 2);
s8_t semi = s8("a;b;\n1;2;\n");
params.delimiter = s8(";");
row = 0;
for (csv_parser_t it = csv_iter(arena, semi, params); csv_is_valid(&it); csv_advance(&it)) {
assert(it.len == 3);
assert(s8_are_equal(it.data[2], s8("")));
row += 1;
}
assert(row == 2);
ma_destroy(arena);
}
fn void test_s8(void) {
ma_arena_t *arena = ma_create(ma_default_reserve_size);
@@ -175,6 +286,8 @@ fn void test_s8(void) {
int main() {
os_core_init();
test_s8();
test_json();
test_csv();
os_test();
return 0;
}