Compare commits
4 Commits
58e28673bc
...
0bea393c13
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bea393c13 | ||
|
|
68f9136ba5 | ||
|
|
f97402bfa4 | ||
|
|
cc5db90495 |
@@ -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"
|
||||
|
||||
@@ -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
180
src/core/core_csv.c
Normal 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
34
src/core/core_csv.h
Normal 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
645
src/core/core_json.c
Normal 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
55
src/core/core_json.h
Normal 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);
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user