diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..0ff570c --- /dev/null +++ b/.clang-format @@ -0,0 +1,11 @@ +BasedOnStyle: LLVM +Language: Cpp +ColumnLimit: 100 +UseTab: ForIndentation +TabWidth: 8 +IndentWidth: 8 +BreakBeforeBraces: Linux +IndentCaseLabels: false +AllowShortFunctionsOnASingleLine: Empty +SortIncludes: false +PointerAlignment: Right diff --git a/Cargo.lock b/Cargo.lock index a38a123..285157d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1053,6 +1053,13 @@ dependencies = [ "thiserror", ] +[[package]] +name = "sidex-gen-c" +version = "0.1.0" +dependencies = [ + "sidex-gen", +] + [[package]] name = "sidex-gen-ir" version = "0.1.0" diff --git a/crates/sidex-gen-c/Cargo.toml b/crates/sidex-gen-c/Cargo.toml new file mode 100644 index 0000000..09a2685 --- /dev/null +++ b/crates/sidex-gen-c/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "sidex-gen-c" +description = "Sidex code generation support for C." +version = "0.1.0" +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true + +[dependencies] +sidex-gen.workspace = true + +[lints] +workspace = true diff --git a/crates/sidex-gen-c/README.md b/crates/sidex-gen-c/README.md new file mode 100644 index 0000000..6e41ee7 --- /dev/null +++ b/crates/sidex-gen-c/README.md @@ -0,0 +1,8 @@ +# Sidex: C Code Generation + +This crate is part of [Sidex](https://oss.silitics.com/sidex/). It contains +the C code generation backend and the small portable C runtime used by +generated C bindings. + +The backend is intentionally generic and independent of any product or RTOS. +The C runtime is C99 and avoids dynamic allocation. diff --git a/crates/sidex-gen-c/fuzz/README.md b/crates/sidex-gen-c/fuzz/README.md new file mode 100644 index 0000000..2cd8426 --- /dev/null +++ b/crates/sidex-gen-c/fuzz/README.md @@ -0,0 +1,51 @@ +# Sidex C JSON Runtime Fuzzing + +This directory contains a libFuzzer target for the portable C JSON runtime. + +Build it with Clang: + +```sh +mkdir -p target/sidex-gen-c-fuzz +clang \ + -std=c99 -g -O1 \ + -fsanitize=fuzzer,address,undefined \ + -I crates/sidex-gen-c/runtime \ + crates/sidex-gen-c/runtime/sidex_types.c \ + crates/sidex-gen-c/runtime/sidex_json_reader.c \ + crates/sidex-gen-c/runtime/sidex_json_value.c \ + crates/sidex-gen-c/runtime/sidex_json_writer.c \ + crates/sidex-gen-c/fuzz/json_runtime_fuzzer.c \ + -o target/sidex-gen-c-fuzz/json_runtime_fuzzer +``` + +Run it with the seed corpus: + +```sh +rm -rf target/sidex-gen-c-fuzz/json_runtime_corpus +mkdir -p target/sidex-gen-c-fuzz/json_runtime_corpus +cp crates/sidex-gen-c/fuzz/corpus/json_runtime/* \ + target/sidex-gen-c-fuzz/json_runtime_corpus/ +target/sidex-gen-c-fuzz/json_runtime_fuzzer \ + target/sidex-gen-c-fuzz/json_runtime_corpus \ + -max_len=16384 \ + -rss_limit_mb=512 +``` + +If Clang is not installed locally, the same commands can be run through Nix: + +```sh +nix --extra-experimental-features 'nix-command flakes' shell nixpkgs#clang nixpkgs#clang-tools -c clang \ + -std=c99 -g -O1 \ + -fsanitize=fuzzer,address,undefined \ + -I crates/sidex-gen-c/runtime \ + crates/sidex-gen-c/runtime/sidex_types.c \ + crates/sidex-gen-c/runtime/sidex_json_reader.c \ + crates/sidex-gen-c/runtime/sidex_json_value.c \ + crates/sidex-gen-c/runtime/sidex_json_writer.c \ + crates/sidex-gen-c/fuzz/json_runtime_fuzzer.c \ + -o target/sidex-gen-c-fuzz/json_runtime_fuzzer +``` + +The fuzzer treats malformed JSON, arena exhaustion, and output-buffer exhaustion +as expected non-crashing outcomes. Successful decodes are encoded, parsed again, +and encoded a second time; the two encoded byte strings must match. diff --git a/crates/sidex-gen-c/fuzz/corpus/json_runtime/edge.json b/crates/sidex-gen-c/fuzz/corpus/json_runtime/edge.json new file mode 100644 index 0000000..ac84145 --- /dev/null +++ b/crates/sidex-gen-c/fuzz/corpus/json_runtime/edge.json @@ -0,0 +1 @@ +{"empty_object":{},"empty_array":[],"numbers":[0,1,-1,9007199254740991,1.0,1e+9]} diff --git a/crates/sidex-gen-c/fuzz/corpus/json_runtime/nested.json b/crates/sidex-gen-c/fuzz/corpus/json_runtime/nested.json new file mode 100644 index 0000000..eeaff0d --- /dev/null +++ b/crates/sidex-gen-c/fuzz/corpus/json_runtime/nested.json @@ -0,0 +1 @@ +{"items":[{"id":1,"tags":["a","b"]},{"id":2,"tags":[]}],"meta":{"ok":true}} diff --git a/crates/sidex-gen-c/fuzz/corpus/json_runtime/null.json b/crates/sidex-gen-c/fuzz/corpus/json_runtime/null.json new file mode 100644 index 0000000..19765bd --- /dev/null +++ b/crates/sidex-gen-c/fuzz/corpus/json_runtime/null.json @@ -0,0 +1 @@ +null diff --git a/crates/sidex-gen-c/fuzz/corpus/json_runtime/scalars.json b/crates/sidex-gen-c/fuzz/corpus/json_runtime/scalars.json new file mode 100644 index 0000000..825bb40 --- /dev/null +++ b/crates/sidex-gen-c/fuzz/corpus/json_runtime/scalars.json @@ -0,0 +1 @@ +[true,false,null,0,-1,12.5e-3,"text","line\nbreak"] diff --git a/crates/sidex-gen-c/fuzz/corpus/json_runtime/unicode.json b/crates/sidex-gen-c/fuzz/corpus/json_runtime/unicode.json new file mode 100644 index 0000000..70f5c8a --- /dev/null +++ b/crates/sidex-gen-c/fuzz/corpus/json_runtime/unicode.json @@ -0,0 +1 @@ +{"raw":"ß🚀","escaped":"\u00df\ud83d\ude80","control":"a\u0000b"} diff --git a/crates/sidex-gen-c/fuzz/json_runtime_fuzzer.c b/crates/sidex-gen-c/fuzz/json_runtime_fuzzer.c new file mode 100644 index 0000000..8dae4ef --- /dev/null +++ b/crates/sidex-gen-c/fuzz/json_runtime_fuzzer.c @@ -0,0 +1,121 @@ +#include "sidex_json.h" + +#include +#include +#include +#include + +#ifndef SIDEX_FUZZ_MAX_INPUT +#define SIDEX_FUZZ_MAX_INPUT 16384u +#endif + +#ifndef SIDEX_FUZZ_ARENA_CAP +#define SIDEX_FUZZ_ARENA_CAP 65536u +#endif + +static void require(bool condition) +{ + if (!condition) { + abort(); + } +} + +static void exercise_skip_value(const uint8_t *data, size_t size) +{ + sidex_json_reader reader; + sidex_json_token_kind kind; + int rc; + + sidex_json_reader_init(&reader, data, size); + rc = sidex_json_skip_value(&reader, SIDEX_JSON_DEFAULT_MAX_DEPTH); + if (rc == SIDEX_OK) { + (void)sidex_json_peek(&reader, &kind); + } +} + +static void exercise_value_round_trip(const uint8_t *data, size_t size) +{ + uint8_t *arena_storage; + uint8_t *second_arena_storage; + uint8_t *output; + uint8_t *second_output; + size_t output_cap; + sidex_arena arena; + sidex_arena second_arena; + sidex_json_reader reader; + sidex_json_reader second_reader; + sidex_json_value value; + sidex_json_value second_value; + sidex_json_buffer buffer; + sidex_json_buffer second_buffer; + sidex_json_writer writer; + sidex_json_writer second_writer; + sidex_json_token_kind kind; + int rc; + + arena_storage = (uint8_t *)malloc(SIDEX_FUZZ_ARENA_CAP); + second_arena_storage = (uint8_t *)malloc(SIDEX_FUZZ_ARENA_CAP); + output_cap = size * 8u + 256u; + output = (uint8_t *)malloc(output_cap); + second_output = (uint8_t *)malloc(output_cap); + if (arena_storage == NULL || second_arena_storage == NULL || output == NULL || + second_output == NULL) { + goto done; + } + + arena = sidex_arena_from_parts(arena_storage, SIDEX_FUZZ_ARENA_CAP); + sidex_json_reader_init(&reader, data, size); + rc = sidex_json_read_value(&reader, &value, &arena, SIDEX_JSON_DEFAULT_MAX_DEPTH); + if (rc != SIDEX_OK) { + goto done; + } + + rc = sidex_json_peek(&reader, &kind); + if (rc != SIDEX_OK) { + goto done; + } + if (kind != SIDEX_JSON_TOKEN_END) { + goto done; + } + + sidex_json_buffer_init(&buffer, output, output_cap); + writer = sidex_json_writer_for_buffer(&buffer); + rc = sidex_json_write_value(&writer, &value, SIDEX_JSON_DEFAULT_MAX_DEPTH); + if (rc != SIDEX_OK) { + goto done; + } + + second_arena = sidex_arena_from_parts(second_arena_storage, SIDEX_FUZZ_ARENA_CAP); + sidex_json_reader_init(&second_reader, output, buffer.len); + rc = sidex_json_read_value(&second_reader, &second_value, &second_arena, + SIDEX_JSON_DEFAULT_MAX_DEPTH); + require(rc == SIDEX_OK); + rc = sidex_json_peek(&second_reader, &kind); + require(rc == SIDEX_OK); + require(kind == SIDEX_JSON_TOKEN_END); + + sidex_json_buffer_init(&second_buffer, second_output, output_cap); + second_writer = sidex_json_writer_for_buffer(&second_buffer); + rc = sidex_json_write_value(&second_writer, &second_value, SIDEX_JSON_DEFAULT_MAX_DEPTH); + require(rc == SIDEX_OK); + require(second_buffer.len == buffer.len); + require(memcmp(second_output, output, buffer.len) == 0); + +done: + free(second_output); + free(output); + free(second_arena_storage); + free(arena_storage); +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + if (size > SIDEX_FUZZ_MAX_INPUT) { + return 0; + } + + (void)sidex_utf8_validate(data, size); + exercise_skip_value(data, size); + exercise_value_round_trip(data, size); + return 0; +} diff --git a/crates/sidex-gen-c/runtime/sidex_json.h b/crates/sidex-gen-c/runtime/sidex_json.h new file mode 100644 index 0000000..79dfcd7 --- /dev/null +++ b/crates/sidex-gen-c/runtime/sidex_json.h @@ -0,0 +1,10 @@ +#ifndef SIDEX_JSON_H +#define SIDEX_JSON_H + +/** Umbrella include for the Sidex JSON reader and writer APIs. */ + +#include "sidex_json_reader.h" +#include "sidex_json_value.h" +#include "sidex_json_writer.h" + +#endif diff --git a/crates/sidex-gen-c/runtime/sidex_json_reader.c b/crates/sidex-gen-c/runtime/sidex_json_reader.c new file mode 100644 index 0000000..468fe49 --- /dev/null +++ b/crates/sidex-gen-c/runtime/sidex_json_reader.c @@ -0,0 +1,777 @@ +#include "sidex_json_reader.h" + +#include +#include + +static bool is_ws(uint8_t ch) +{ + return ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t'; +} + +static bool is_digit(uint8_t ch) +{ + return ch >= '0' && ch <= '9'; +} + +static void skip_ws(sidex_json_reader *reader) +{ + while (reader->pos < reader->len && is_ws(reader->ptr[reader->pos])) { + reader->pos++; + } +} + +static int expect_literal(sidex_json_reader *reader, const char *literal, size_t len) +{ + skip_ws(reader); + if (reader->len - reader->pos < len) { + return SIDEX_ERR_EOF; + } + if (memcmp(reader->ptr + reader->pos, literal, len) != 0) { + return SIDEX_ERR_EXPECTED; + } + reader->pos += len; + return SIDEX_OK; +} + +static int hex_digit(uint8_t ch) +{ + if (ch >= '0' && ch <= '9') { + return (int)(ch - '0'); + } + if (ch >= 'a' && ch <= 'f') { + return 10 + (int)(ch - 'a'); + } + if (ch >= 'A' && ch <= 'F') { + return 10 + (int)(ch - 'A'); + } + return -1; +} + +static int parse_hex4(const uint8_t *ptr, uint32_t *out) +{ + uint32_t value = 0; + + for (size_t i = 0; i < 4; i++) { + int digit = hex_digit(ptr[i]); + + if (digit < 0) { + return SIDEX_ERR_SYNTAX; + } + value = (value << 4) | (uint32_t)digit; + } + *out = value; + return SIDEX_OK; +} + +static int arena_append(sidex_arena *arena, uint8_t byte) +{ + if (arena == NULL || arena->ptr == NULL || arena->len >= arena->cap) { + return SIDEX_ERR_NO_SPACE; + } + arena->ptr[arena->len] = byte; + arena->len += 1; + return SIDEX_OK; +} + +static int arena_append_slice(sidex_arena *arena, const uint8_t *ptr, size_t len) +{ + if (len == 0) { + return SIDEX_OK; + } + if (arena == NULL || arena->ptr == NULL || arena->len > arena->cap || + len > arena->cap - arena->len) { + return SIDEX_ERR_NO_SPACE; + } + memcpy(arena->ptr + arena->len, ptr, len); + arena->len += len; + return SIDEX_OK; +} + +static int arena_append_utf8(sidex_arena *arena, uint32_t scalar) +{ + if (scalar <= 0x7fu) { + return arena_append(arena, (uint8_t)scalar); + } + if (scalar <= 0x7ffu) { + int rc = arena_append(arena, (uint8_t)(0xc0u | (scalar >> 6))); + if (rc != SIDEX_OK) { + return rc; + } + return arena_append(arena, (uint8_t)(0x80u | (scalar & 0x3fu))); + } + if (scalar >= 0xd800u && scalar <= 0xdfffu) { + return SIDEX_ERR_UTF8; + } + if (scalar <= 0xffffu) { + int rc = arena_append(arena, (uint8_t)(0xe0u | (scalar >> 12))); + if (rc != SIDEX_OK) { + return rc; + } + rc = arena_append(arena, (uint8_t)(0x80u | ((scalar >> 6) & 0x3fu))); + if (rc != SIDEX_OK) { + return rc; + } + return arena_append(arena, (uint8_t)(0x80u | (scalar & 0x3fu))); + } + if (scalar <= 0x10ffffu) { + int rc = arena_append(arena, (uint8_t)(0xf0u | (scalar >> 18))); + if (rc != SIDEX_OK) { + return rc; + } + rc = arena_append(arena, (uint8_t)(0x80u | ((scalar >> 12) & 0x3fu))); + if (rc != SIDEX_OK) { + return rc; + } + rc = arena_append(arena, (uint8_t)(0x80u | ((scalar >> 6) & 0x3fu))); + if (rc != SIDEX_OK) { + return rc; + } + return arena_append(arena, (uint8_t)(0x80u | (scalar & 0x3fu))); + } + return SIDEX_ERR_UTF8; +} + +static int arena_rollback(sidex_arena *arena, size_t arena_start, int rc) +{ + if (arena != NULL && arena->len >= arena_start) { + arena->len = arena_start; + } + return rc; +} + +void sidex_json_reader_init(sidex_json_reader *reader, const uint8_t *ptr, size_t len) +{ + reader->ptr = ptr; + reader->len = len; + reader->pos = 0; +} + +int sidex_json_peek(sidex_json_reader *reader, sidex_json_token_kind *kind) +{ + skip_ws(reader); + if (reader->pos >= reader->len) { + *kind = SIDEX_JSON_TOKEN_END; + return SIDEX_OK; + } + + switch (reader->ptr[reader->pos]) { + case '{': + *kind = SIDEX_JSON_TOKEN_OBJECT_BEGIN; + return SIDEX_OK; + case '}': + *kind = SIDEX_JSON_TOKEN_OBJECT_END; + return SIDEX_OK; + case '[': + *kind = SIDEX_JSON_TOKEN_ARRAY_BEGIN; + return SIDEX_OK; + case ']': + *kind = SIDEX_JSON_TOKEN_ARRAY_END; + return SIDEX_OK; + case ':': + *kind = SIDEX_JSON_TOKEN_NAME_SEPARATOR; + return SIDEX_OK; + case ',': + *kind = SIDEX_JSON_TOKEN_VALUE_SEPARATOR; + return SIDEX_OK; + case '"': + *kind = SIDEX_JSON_TOKEN_STRING; + return SIDEX_OK; + case 't': + *kind = SIDEX_JSON_TOKEN_TRUE; + return SIDEX_OK; + case 'f': + *kind = SIDEX_JSON_TOKEN_FALSE; + return SIDEX_OK; + case 'n': + *kind = SIDEX_JSON_TOKEN_NULL; + return SIDEX_OK; + case '-': + *kind = SIDEX_JSON_TOKEN_NUMBER; + return SIDEX_OK; + default: + if (is_digit(reader->ptr[reader->pos])) { + *kind = SIDEX_JSON_TOKEN_NUMBER; + return SIDEX_OK; + } + return SIDEX_ERR_SYNTAX; + } +} + +int sidex_json_expect(sidex_json_reader *reader, sidex_json_token_kind kind) +{ + sidex_json_token_kind actual; + int rc = sidex_json_peek(reader, &actual); + + if (rc != SIDEX_OK) { + return rc; + } + if (actual != kind) { + return SIDEX_ERR_EXPECTED; + } + + switch (kind) { + case SIDEX_JSON_TOKEN_OBJECT_BEGIN: + case SIDEX_JSON_TOKEN_OBJECT_END: + case SIDEX_JSON_TOKEN_ARRAY_BEGIN: + case SIDEX_JSON_TOKEN_ARRAY_END: + case SIDEX_JSON_TOKEN_NAME_SEPARATOR: + case SIDEX_JSON_TOKEN_VALUE_SEPARATOR: + reader->pos++; + return SIDEX_OK; + case SIDEX_JSON_TOKEN_TRUE: + return expect_literal(reader, "true", 4); + case SIDEX_JSON_TOKEN_FALSE: + return expect_literal(reader, "false", 5); + case SIDEX_JSON_TOKEN_NULL: + return expect_literal(reader, "null", 4); + default: + return SIDEX_ERR_UNSUPPORTED; + } +} + +int sidex_json_read_bool(sidex_json_reader *reader, bool *out) +{ + sidex_json_token_kind kind; + int rc = sidex_json_peek(reader, &kind); + + if (rc != SIDEX_OK) { + return rc; + } + if (kind == SIDEX_JSON_TOKEN_TRUE) { + rc = expect_literal(reader, "true", 4); + *out = true; + return rc; + } + if (kind == SIDEX_JSON_TOKEN_FALSE) { + rc = expect_literal(reader, "false", 5); + *out = false; + return rc; + } + return SIDEX_ERR_EXPECTED; +} + +int sidex_json_read_null(sidex_json_reader *reader) +{ + return sidex_json_expect(reader, SIDEX_JSON_TOKEN_NULL); +} + +static int parse_uint_digits(sidex_json_reader *reader, uint64_t limit, uint64_t *out) +{ + uint64_t value = 0; + size_t start = reader->pos; + + if (reader->pos >= reader->len || !is_digit(reader->ptr[reader->pos])) { + return SIDEX_ERR_EXPECTED; + } + + if (reader->ptr[reader->pos] == '0') { + reader->pos++; + if (reader->pos < reader->len && is_digit(reader->ptr[reader->pos])) { + return SIDEX_ERR_SYNTAX; + } + *out = 0; + return SIDEX_OK; + } + + while (reader->pos < reader->len && is_digit(reader->ptr[reader->pos])) { + uint64_t digit = (uint64_t)(reader->ptr[reader->pos] - '0'); + + if (value > (limit - digit) / 10u) { + return SIDEX_ERR_OVERFLOW; + } + value = value * 10u + digit; + reader->pos++; + } + + if (reader->pos == start) { + return SIDEX_ERR_EXPECTED; + } + *out = value; + return SIDEX_OK; +} + +static int reject_fraction_or_exponent(sidex_json_reader *reader) +{ + if (reader->pos < reader->len && + (reader->ptr[reader->pos] == '.' || reader->ptr[reader->pos] == 'e' || + reader->ptr[reader->pos] == 'E')) { + return SIDEX_ERR_UNSUPPORTED; + } + return SIDEX_OK; +} + +int sidex_json_read_u64(sidex_json_reader *reader, uint64_t *out) +{ + int rc; + + skip_ws(reader); + if (reader->pos < reader->len && reader->ptr[reader->pos] == '-') { + return SIDEX_ERR_EXPECTED; + } + rc = parse_uint_digits(reader, UINT64_MAX, out); + if (rc != SIDEX_OK) { + return rc; + } + return reject_fraction_or_exponent(reader); +} + +int sidex_json_read_i64(sidex_json_reader *reader, int64_t *out) +{ + bool negative = false; + uint64_t value; + uint64_t limit = (uint64_t)INT64_MAX; + int rc; + + skip_ws(reader); + if (reader->pos < reader->len && reader->ptr[reader->pos] == '-') { + negative = true; + reader->pos++; + limit += 1u; + } + + rc = parse_uint_digits(reader, limit, &value); + if (rc != SIDEX_OK) { + return rc; + } + rc = reject_fraction_or_exponent(reader); + if (rc != SIDEX_OK) { + return rc; + } + + if (negative) { + if (value == limit) { + *out = INT64_MIN; + } else { + *out = -(int64_t)value; + } + } else { + *out = (int64_t)value; + } + return SIDEX_OK; +} + +static int decode_unicode_escape(sidex_json_reader *reader, sidex_arena *arena) +{ + uint32_t scalar; + int rc; + + if (reader->len - reader->pos < 4) { + return SIDEX_ERR_EOF; + } + rc = parse_hex4(reader->ptr + reader->pos, &scalar); + if (rc != SIDEX_OK) { + return rc; + } + reader->pos += 4; + + if (scalar >= 0xd800u && scalar <= 0xdbffu) { + uint32_t low; + + if (reader->len - reader->pos < 6 || reader->ptr[reader->pos] != '\\' || + reader->ptr[reader->pos + 1] != 'u') { + return SIDEX_ERR_UTF8; + } + reader->pos += 2; + rc = parse_hex4(reader->ptr + reader->pos, &low); + if (rc != SIDEX_OK) { + return rc; + } + reader->pos += 4; + if (low < 0xdc00u || low > 0xdfffu) { + return SIDEX_ERR_UTF8; + } + scalar = 0x10000u + (((scalar - 0xd800u) << 10) | (low - 0xdc00u)); + } else if (scalar >= 0xdc00u && scalar <= 0xdfffu) { + return SIDEX_ERR_UTF8; + } + + return arena_append_utf8(arena, scalar); +} + +int sidex_json_read_str(sidex_json_reader *reader, sidex_str *out, sidex_arena *arena) +{ + size_t string_start; + size_t copy_start; + size_t arena_start = arena != NULL ? arena->len : 0; + bool escaped = false; + + skip_ws(reader); + if (reader->pos >= reader->len) { + return arena_rollback(arena, arena_start, SIDEX_ERR_EOF); + } + if (reader->ptr[reader->pos] != '"') { + return arena_rollback(arena, arena_start, SIDEX_ERR_EXPECTED); + } + reader->pos++; + string_start = reader->pos; + copy_start = reader->pos; + + while (reader->pos < reader->len) { + uint8_t ch = reader->ptr[reader->pos]; + + if (ch == '"') { + const uint8_t *ptr; + size_t len; + + if (escaped) { + int rc = arena_append_slice(arena, reader->ptr + copy_start, + reader->pos - copy_start); + if (rc != SIDEX_OK) { + return arena_rollback(arena, arena_start, rc); + } + ptr = arena->ptr + arena_start; + len = arena->len - arena_start; + } else { + ptr = reader->ptr + string_start; + len = reader->pos - string_start; + } + if (!sidex_utf8_validate(ptr, len)) { + return arena_rollback(arena, arena_start, SIDEX_ERR_UTF8); + } + reader->pos++; + *out = sidex_str_from_parts(ptr, len); + return SIDEX_OK; + } + + if (ch < 0x20u) { + return arena_rollback(arena, arena_start, SIDEX_ERR_SYNTAX); + } + + if (ch != '\\') { + reader->pos++; + continue; + } + + escaped = true; + int rc = + arena_append_slice(arena, reader->ptr + copy_start, reader->pos - copy_start); + if (rc != SIDEX_OK) { + return arena_rollback(arena, arena_start, rc); + } + + reader->pos++; + if (reader->pos >= reader->len) { + return arena_rollback(arena, arena_start, SIDEX_ERR_EOF); + } + + ch = reader->ptr[reader->pos++]; + switch (ch) { + case '"': + case '\\': + case '/': + rc = arena_append(arena, ch); + break; + case 'b': + rc = arena_append(arena, '\b'); + break; + case 'f': + rc = arena_append(arena, '\f'); + break; + case 'n': + rc = arena_append(arena, '\n'); + break; + case 'r': + rc = arena_append(arena, '\r'); + break; + case 't': + rc = arena_append(arena, '\t'); + break; + case 'u': + rc = decode_unicode_escape(reader, arena); + break; + default: + return arena_rollback(arena, arena_start, SIDEX_ERR_SYNTAX); + } + if (rc != SIDEX_OK) { + return arena_rollback(arena, arena_start, rc); + } + copy_start = reader->pos; + } + + return arena_rollback(arena, arena_start, SIDEX_ERR_EOF); +} + +static int skip_unicode_escape(sidex_json_reader *reader) +{ + uint32_t scalar; + int rc; + + if (reader->len - reader->pos < 4) { + return SIDEX_ERR_EOF; + } + rc = parse_hex4(reader->ptr + reader->pos, &scalar); + if (rc != SIDEX_OK) { + return rc; + } + reader->pos += 4; + + if (scalar >= 0xd800u && scalar <= 0xdbffu) { + uint32_t low; + + if (reader->len - reader->pos < 6 || reader->ptr[reader->pos] != '\\' || + reader->ptr[reader->pos + 1] != 'u') { + return SIDEX_ERR_UTF8; + } + reader->pos += 2; + rc = parse_hex4(reader->ptr + reader->pos, &low); + if (rc != SIDEX_OK) { + return rc; + } + reader->pos += 4; + if (low < 0xdc00u || low > 0xdfffu) { + return SIDEX_ERR_UTF8; + } + } else if (scalar >= 0xdc00u && scalar <= 0xdfffu) { + return SIDEX_ERR_UTF8; + } + + return SIDEX_OK; +} + +static int skip_string_token(sidex_json_reader *reader) +{ + size_t chunk_start; + + skip_ws(reader); + if (reader->pos >= reader->len) { + return SIDEX_ERR_EOF; + } + if (reader->ptr[reader->pos] != '"') { + return SIDEX_ERR_EXPECTED; + } + reader->pos++; + chunk_start = reader->pos; + + while (reader->pos < reader->len) { + uint8_t ch = reader->ptr[reader->pos]; + + if (ch == '"') { + if (!sidex_utf8_validate(reader->ptr + chunk_start, + reader->pos - chunk_start)) { + return SIDEX_ERR_UTF8; + } + reader->pos++; + return SIDEX_OK; + } + if (ch < 0x20u) { + return SIDEX_ERR_SYNTAX; + } + if (ch != '\\') { + reader->pos++; + continue; + } + + if (!sidex_utf8_validate(reader->ptr + chunk_start, reader->pos - chunk_start)) { + return SIDEX_ERR_UTF8; + } + reader->pos++; + if (reader->pos >= reader->len) { + return SIDEX_ERR_EOF; + } + + ch = reader->ptr[reader->pos++]; + switch (ch) { + case '"': + case '\\': + case '/': + case 'b': + case 'f': + case 'n': + case 'r': + case 't': + break; + case 'u': { + int rc = skip_unicode_escape(reader); + if (rc != SIDEX_OK) { + return rc; + } + break; + } + default: + return SIDEX_ERR_SYNTAX; + } + chunk_start = reader->pos; + } + + return SIDEX_ERR_EOF; +} + +int sidex_json_read_number(sidex_json_reader *reader, sidex_str *out) +{ + size_t start; + + skip_ws(reader); + start = reader->pos; + if (reader->pos < reader->len && reader->ptr[reader->pos] == '-') { + reader->pos++; + } + if (reader->pos >= reader->len || !is_digit(reader->ptr[reader->pos])) { + return SIDEX_ERR_EXPECTED; + } + if (reader->ptr[reader->pos] == '0') { + reader->pos++; + if (reader->pos < reader->len && is_digit(reader->ptr[reader->pos])) { + return SIDEX_ERR_SYNTAX; + } + } else { + while (reader->pos < reader->len && is_digit(reader->ptr[reader->pos])) { + reader->pos++; + } + } + if (reader->pos < reader->len && reader->ptr[reader->pos] == '.') { + reader->pos++; + if (reader->pos >= reader->len || !is_digit(reader->ptr[reader->pos])) { + return SIDEX_ERR_SYNTAX; + } + while (reader->pos < reader->len && is_digit(reader->ptr[reader->pos])) { + reader->pos++; + } + } + if (reader->pos < reader->len && + (reader->ptr[reader->pos] == 'e' || reader->ptr[reader->pos] == 'E')) { + reader->pos++; + if (reader->pos < reader->len && + (reader->ptr[reader->pos] == '+' || reader->ptr[reader->pos] == '-')) { + reader->pos++; + } + if (reader->pos >= reader->len || !is_digit(reader->ptr[reader->pos])) { + return SIDEX_ERR_SYNTAX; + } + while (reader->pos < reader->len && is_digit(reader->ptr[reader->pos])) { + reader->pos++; + } + } + *out = sidex_str_from_parts(reader->ptr + start, reader->pos - start); + return SIDEX_OK; +} + +static int skip_number(sidex_json_reader *reader) +{ + sidex_str ignored; + + return sidex_json_read_number(reader, &ignored); +} + +static int skip_value_with_depth(sidex_json_reader *reader, size_t max_depth); + +static int skip_array(sidex_json_reader *reader, size_t max_depth) +{ + sidex_json_token_kind kind; + int rc = sidex_json_expect(reader, SIDEX_JSON_TOKEN_ARRAY_BEGIN); + + if (rc != SIDEX_OK) { + return rc; + } + rc = sidex_json_peek(reader, &kind); + if (rc != SIDEX_OK) { + return rc; + } + if (kind == SIDEX_JSON_TOKEN_ARRAY_END) { + return sidex_json_expect(reader, SIDEX_JSON_TOKEN_ARRAY_END); + } + + while (true) { + rc = skip_value_with_depth(reader, max_depth - 1u); + if (rc != SIDEX_OK) { + return rc; + } + rc = sidex_json_peek(reader, &kind); + if (rc != SIDEX_OK) { + return rc; + } + if (kind == SIDEX_JSON_TOKEN_ARRAY_END) { + return sidex_json_expect(reader, SIDEX_JSON_TOKEN_ARRAY_END); + } + if (kind != SIDEX_JSON_TOKEN_VALUE_SEPARATOR) { + return SIDEX_ERR_EXPECTED; + } + rc = sidex_json_expect(reader, SIDEX_JSON_TOKEN_VALUE_SEPARATOR); + if (rc != SIDEX_OK) { + return rc; + } + } +} + +static int skip_object(sidex_json_reader *reader, size_t max_depth) +{ + sidex_json_token_kind kind; + int rc = sidex_json_expect(reader, SIDEX_JSON_TOKEN_OBJECT_BEGIN); + + if (rc != SIDEX_OK) { + return rc; + } + rc = sidex_json_peek(reader, &kind); + if (rc != SIDEX_OK) { + return rc; + } + if (kind == SIDEX_JSON_TOKEN_OBJECT_END) { + return sidex_json_expect(reader, SIDEX_JSON_TOKEN_OBJECT_END); + } + + while (true) { + rc = skip_string_token(reader); + if (rc != SIDEX_OK) { + return rc; + } + rc = sidex_json_expect(reader, SIDEX_JSON_TOKEN_NAME_SEPARATOR); + if (rc != SIDEX_OK) { + return rc; + } + rc = skip_value_with_depth(reader, max_depth - 1u); + if (rc != SIDEX_OK) { + return rc; + } + rc = sidex_json_peek(reader, &kind); + if (rc != SIDEX_OK) { + return rc; + } + if (kind == SIDEX_JSON_TOKEN_OBJECT_END) { + return sidex_json_expect(reader, SIDEX_JSON_TOKEN_OBJECT_END); + } + if (kind != SIDEX_JSON_TOKEN_VALUE_SEPARATOR) { + return SIDEX_ERR_EXPECTED; + } + rc = sidex_json_expect(reader, SIDEX_JSON_TOKEN_VALUE_SEPARATOR); + if (rc != SIDEX_OK) { + return rc; + } + } +} + +static int skip_value_with_depth(sidex_json_reader *reader, size_t max_depth) +{ + sidex_json_token_kind kind; + int rc; + + if (max_depth == 0) { + return SIDEX_ERR_DEPTH; + } + + rc = sidex_json_peek(reader, &kind); + if (rc != SIDEX_OK) { + return rc; + } + switch (kind) { + case SIDEX_JSON_TOKEN_OBJECT_BEGIN: + return skip_object(reader, max_depth); + case SIDEX_JSON_TOKEN_ARRAY_BEGIN: + return skip_array(reader, max_depth); + case SIDEX_JSON_TOKEN_STRING: + return skip_string_token(reader); + case SIDEX_JSON_TOKEN_NUMBER: + return skip_number(reader); + case SIDEX_JSON_TOKEN_TRUE: + return sidex_json_expect(reader, SIDEX_JSON_TOKEN_TRUE); + case SIDEX_JSON_TOKEN_FALSE: + return sidex_json_expect(reader, SIDEX_JSON_TOKEN_FALSE); + case SIDEX_JSON_TOKEN_NULL: + return sidex_json_expect(reader, SIDEX_JSON_TOKEN_NULL); + default: + return SIDEX_ERR_EXPECTED; + } +} + +int sidex_json_skip_value(sidex_json_reader *reader, size_t max_depth) +{ + return skip_value_with_depth(reader, max_depth); +} diff --git a/crates/sidex-gen-c/runtime/sidex_json_reader.h b/crates/sidex-gen-c/runtime/sidex_json_reader.h new file mode 100644 index 0000000..212b03c --- /dev/null +++ b/crates/sidex-gen-c/runtime/sidex_json_reader.h @@ -0,0 +1,79 @@ +#ifndef SIDEX_JSON_READER_H +#define SIDEX_JSON_READER_H + +#include "sidex_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** Token kinds visible to the low-level streaming JSON reader. */ +typedef enum sidex_json_token_kind { + SIDEX_JSON_TOKEN_END = 0, + SIDEX_JSON_TOKEN_OBJECT_BEGIN, + SIDEX_JSON_TOKEN_OBJECT_END, + SIDEX_JSON_TOKEN_ARRAY_BEGIN, + SIDEX_JSON_TOKEN_ARRAY_END, + SIDEX_JSON_TOKEN_NAME_SEPARATOR, + SIDEX_JSON_TOKEN_VALUE_SEPARATOR, + SIDEX_JSON_TOKEN_STRING, + SIDEX_JSON_TOKEN_NUMBER, + SIDEX_JSON_TOKEN_TRUE, + SIDEX_JSON_TOKEN_FALSE, + SIDEX_JSON_TOKEN_NULL, +} sidex_json_token_kind; + +#define SIDEX_JSON_DEFAULT_MAX_DEPTH ((size_t)128u) + +/** Streaming reader over a caller-owned JSON byte slice. */ +typedef struct sidex_json_reader { + const uint8_t *ptr; + size_t len; + size_t pos; +} sidex_json_reader; + +/** Initializes a reader over a non-NUL-terminated JSON byte slice. */ +void sidex_json_reader_init(sidex_json_reader *reader, const uint8_t *ptr, size_t len); + +/** Peeks at the next token without consuming it, ignoring leading whitespace. */ +int sidex_json_peek(sidex_json_reader *reader, sidex_json_token_kind *kind); + +/** Consumes the next token if it matches `kind`. */ +int sidex_json_expect(sidex_json_reader *reader, sidex_json_token_kind kind); + +/** Reads a JSON boolean value. */ +int sidex_json_read_bool(sidex_json_reader *reader, bool *out); + +/** Reads a JSON null value. */ +int sidex_json_read_null(sidex_json_reader *reader); + +/** Reads a JSON integer into a signed 64-bit value. Fractions are unsupported. */ +int sidex_json_read_i64(sidex_json_reader *reader, int64_t *out); + +/** Reads a JSON integer into an unsigned 64-bit value. Fractions are unsupported. */ +int sidex_json_read_u64(sidex_json_reader *reader, uint64_t *out); + +/** Reads a JSON number lexeme. */ +int sidex_json_read_number(sidex_json_reader *reader, sidex_str *out); + +/** + * Reads a JSON string. + * + * Unescaped strings are borrowed from the input. Escaped strings are decoded + * into `arena`, and the returned string borrows from it. + */ +int sidex_json_read_str(sidex_json_reader *reader, sidex_str *out, sidex_arena *arena); + +/** + * Consumes one complete JSON value without materializing it. + * + * `max_depth` bounds recursive value nesting. The top-level value consumes one + * level, so a scalar requires at least 1 and `[0]` requires at least 2. + */ +int sidex_json_skip_value(sidex_json_reader *reader, size_t max_depth); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/crates/sidex-gen-c/runtime/sidex_json_value.c b/crates/sidex-gen-c/runtime/sidex_json_value.c new file mode 100644 index 0000000..3a64897 --- /dev/null +++ b/crates/sidex-gen-c/runtime/sidex_json_value.c @@ -0,0 +1,425 @@ +#include "sidex_json_value.h" + +static int value_rollback(sidex_arena *arena, size_t arena_start, int rc) +{ + if (arena != NULL && arena->len >= arena_start) { + arena->len = arena_start; + } + return rc; +} + +static int read_value_with_depth(sidex_json_reader *reader, sidex_json_value *out, + sidex_arena *arena, size_t max_depth); + +static int write_value_with_depth(sidex_json_writer *writer, const sidex_json_value *value, + size_t max_depth); + +static int count_array_items(const sidex_json_reader *reader, size_t *out, size_t max_depth) +{ + sidex_json_reader counter = *reader; + sidex_json_token_kind kind; + size_t count = 0; + int rc; + + if (max_depth == 0) { + return SIDEX_ERR_DEPTH; + } + + rc = sidex_json_expect(&counter, SIDEX_JSON_TOKEN_ARRAY_BEGIN); + if (rc != SIDEX_OK) { + return rc; + } + rc = sidex_json_peek(&counter, &kind); + if (rc != SIDEX_OK) { + return rc; + } + if (kind == SIDEX_JSON_TOKEN_ARRAY_END) { + *out = 0; + return SIDEX_OK; + } + + while (true) { + if (count == SIZE_MAX) { + return SIDEX_ERR_OVERFLOW; + } + count++; + rc = sidex_json_skip_value(&counter, max_depth - 1u); + if (rc != SIDEX_OK) { + return rc; + } + rc = sidex_json_peek(&counter, &kind); + if (rc != SIDEX_OK) { + return rc; + } + if (kind == SIDEX_JSON_TOKEN_ARRAY_END) { + *out = count; + return SIDEX_OK; + } + if (kind != SIDEX_JSON_TOKEN_VALUE_SEPARATOR) { + return SIDEX_ERR_EXPECTED; + } + rc = sidex_json_expect(&counter, SIDEX_JSON_TOKEN_VALUE_SEPARATOR); + if (rc != SIDEX_OK) { + return rc; + } + } +} + +static int count_object_entries(const sidex_json_reader *reader, size_t *out, size_t max_depth) +{ + sidex_json_reader counter = *reader; + sidex_json_token_kind kind; + size_t count = 0; + int rc; + + if (max_depth == 0) { + return SIDEX_ERR_DEPTH; + } + + rc = sidex_json_expect(&counter, SIDEX_JSON_TOKEN_OBJECT_BEGIN); + if (rc != SIDEX_OK) { + return rc; + } + rc = sidex_json_peek(&counter, &kind); + if (rc != SIDEX_OK) { + return rc; + } + if (kind == SIDEX_JSON_TOKEN_OBJECT_END) { + *out = 0; + return SIDEX_OK; + } + + while (true) { + if (count == SIZE_MAX) { + return SIDEX_ERR_OVERFLOW; + } + count++; + rc = sidex_json_peek(&counter, &kind); + if (rc != SIDEX_OK) { + return rc; + } + if (kind != SIDEX_JSON_TOKEN_STRING) { + return SIDEX_ERR_EXPECTED; + } + rc = sidex_json_skip_value(&counter, max_depth - 1u); + if (rc != SIDEX_OK) { + return rc; + } + rc = sidex_json_expect(&counter, SIDEX_JSON_TOKEN_NAME_SEPARATOR); + if (rc != SIDEX_OK) { + return rc; + } + rc = sidex_json_skip_value(&counter, max_depth - 1u); + if (rc != SIDEX_OK) { + return rc; + } + rc = sidex_json_peek(&counter, &kind); + if (rc != SIDEX_OK) { + return rc; + } + if (kind == SIDEX_JSON_TOKEN_OBJECT_END) { + *out = count; + return SIDEX_OK; + } + if (kind != SIDEX_JSON_TOKEN_VALUE_SEPARATOR) { + return SIDEX_ERR_EXPECTED; + } + rc = sidex_json_expect(&counter, SIDEX_JSON_TOKEN_VALUE_SEPARATOR); + if (rc != SIDEX_OK) { + return rc; + } + } +} + +static int read_array_value(sidex_json_reader *reader, sidex_json_value *out, sidex_arena *arena, + size_t max_depth) +{ + size_t len; + sidex_json_value *items = NULL; + int rc; + + rc = count_array_items(reader, &len, max_depth); + if (rc != SIDEX_OK) { + return rc; + } + if (len > 0) { + items = (sidex_json_value *)sidex_arena_alloc_array( + arena, len, sizeof(sidex_json_value), SIDEX_ALIGNOF(sidex_json_value)); + if (items == NULL) { + return SIDEX_ERR_NO_SPACE; + } + } + + rc = sidex_json_expect(reader, SIDEX_JSON_TOKEN_ARRAY_BEGIN); + if (rc != SIDEX_OK) { + return rc; + } + for (size_t i = 0; i < len; i++) { + if (i > 0) { + rc = sidex_json_expect(reader, SIDEX_JSON_TOKEN_VALUE_SEPARATOR); + if (rc != SIDEX_OK) { + return rc; + } + } + rc = read_value_with_depth(reader, &items[i], arena, max_depth - 1u); + if (rc != SIDEX_OK) { + return rc; + } + } + rc = sidex_json_expect(reader, SIDEX_JSON_TOKEN_ARRAY_END); + if (rc != SIDEX_OK) { + return rc; + } + + out->kind = SIDEX_JSON_VALUE_ARRAY; + out->as.array.items = items; + out->as.array.len = len; + return SIDEX_OK; +} + +static int read_object_value(sidex_json_reader *reader, sidex_json_value *out, sidex_arena *arena, + size_t max_depth) +{ + size_t len; + sidex_json_entry *entries = NULL; + int rc; + + rc = count_object_entries(reader, &len, max_depth); + if (rc != SIDEX_OK) { + return rc; + } + if (len > 0) { + entries = (sidex_json_entry *)sidex_arena_alloc_array( + arena, len, sizeof(sidex_json_entry), SIDEX_ALIGNOF(sidex_json_entry)); + if (entries == NULL) { + return SIDEX_ERR_NO_SPACE; + } + } + + rc = sidex_json_expect(reader, SIDEX_JSON_TOKEN_OBJECT_BEGIN); + if (rc != SIDEX_OK) { + return rc; + } + for (size_t i = 0; i < len; i++) { + if (i > 0) { + rc = sidex_json_expect(reader, SIDEX_JSON_TOKEN_VALUE_SEPARATOR); + if (rc != SIDEX_OK) { + return rc; + } + } + rc = sidex_json_read_str(reader, &entries[i].key, arena); + if (rc != SIDEX_OK) { + return rc; + } + rc = sidex_json_expect(reader, SIDEX_JSON_TOKEN_NAME_SEPARATOR); + if (rc != SIDEX_OK) { + return rc; + } + rc = read_value_with_depth(reader, &entries[i].value, arena, max_depth - 1u); + if (rc != SIDEX_OK) { + return rc; + } + } + rc = sidex_json_expect(reader, SIDEX_JSON_TOKEN_OBJECT_END); + if (rc != SIDEX_OK) { + return rc; + } + + out->kind = SIDEX_JSON_VALUE_OBJECT; + out->as.object.entries = entries; + out->as.object.len = len; + return SIDEX_OK; +} + +static int read_value_with_depth(sidex_json_reader *reader, sidex_json_value *out, + sidex_arena *arena, size_t max_depth) +{ + sidex_json_token_kind kind; + size_t arena_start = arena != NULL ? arena->len : 0; + int rc; + + if (max_depth == 0) { + return value_rollback(arena, arena_start, SIDEX_ERR_DEPTH); + } + + rc = sidex_json_peek(reader, &kind); + if (rc != SIDEX_OK) { + return value_rollback(arena, arena_start, rc); + } + + switch (kind) { + case SIDEX_JSON_TOKEN_OBJECT_BEGIN: + rc = read_object_value(reader, out, arena, max_depth); + break; + case SIDEX_JSON_TOKEN_ARRAY_BEGIN: + rc = read_array_value(reader, out, arena, max_depth); + break; + case SIDEX_JSON_TOKEN_STRING: + out->kind = SIDEX_JSON_VALUE_STRING; + rc = sidex_json_read_str(reader, &out->as.string, arena); + break; + case SIDEX_JSON_TOKEN_NUMBER: + out->kind = SIDEX_JSON_VALUE_NUMBER; + rc = sidex_json_read_number(reader, &out->as.number); + break; + case SIDEX_JSON_TOKEN_TRUE: + out->kind = SIDEX_JSON_VALUE_BOOL; + out->as.boolean = true; + rc = sidex_json_expect(reader, SIDEX_JSON_TOKEN_TRUE); + break; + case SIDEX_JSON_TOKEN_FALSE: + out->kind = SIDEX_JSON_VALUE_BOOL; + out->as.boolean = false; + rc = sidex_json_expect(reader, SIDEX_JSON_TOKEN_FALSE); + break; + case SIDEX_JSON_TOKEN_NULL: + out->kind = SIDEX_JSON_VALUE_NULL; + rc = sidex_json_expect(reader, SIDEX_JSON_TOKEN_NULL); + break; + default: + rc = SIDEX_ERR_EXPECTED; + break; + } + + return rc == SIDEX_OK ? SIDEX_OK : value_rollback(arena, arena_start, rc); +} + +int sidex_json_read_value(sidex_json_reader *reader, sidex_json_value *out, sidex_arena *arena, + size_t max_depth) +{ + return read_value_with_depth(reader, out, arena, max_depth); +} + +static bool is_digit(uint8_t ch) +{ + return ch >= '0' && ch <= '9'; +} + +static bool number_lexeme_is_valid(sidex_str number) +{ + size_t pos = 0; + + if (pos < number.len && number.ptr[pos] == '-') { + pos++; + } + if (pos >= number.len || !is_digit(number.ptr[pos])) { + return false; + } + if (number.ptr[pos] == '0') { + pos++; + if (pos < number.len && is_digit(number.ptr[pos])) { + return false; + } + } else { + while (pos < number.len && is_digit(number.ptr[pos])) { + pos++; + } + } + if (pos < number.len && number.ptr[pos] == '.') { + pos++; + if (pos >= number.len || !is_digit(number.ptr[pos])) { + return false; + } + while (pos < number.len && is_digit(number.ptr[pos])) { + pos++; + } + } + if (pos < number.len && (number.ptr[pos] == 'e' || number.ptr[pos] == 'E')) { + pos++; + if (pos < number.len && (number.ptr[pos] == '+' || number.ptr[pos] == '-')) { + pos++; + } + if (pos >= number.len || !is_digit(number.ptr[pos])) { + return false; + } + while (pos < number.len && is_digit(number.ptr[pos])) { + pos++; + } + } + + return pos == number.len; +} + +static int write_array_value(sidex_json_writer *writer, const sidex_json_array *array, + size_t max_depth) +{ + int rc = sidex_json_write_array_begin(writer); + + if (rc != SIDEX_OK) { + return rc; + } + for (size_t i = 0; i < array->len; i++) { + if (i > 0) { + rc = sidex_json_write_value_separator(writer); + if (rc != SIDEX_OK) { + return rc; + } + } + rc = write_value_with_depth(writer, &array->items[i], max_depth - 1u); + if (rc != SIDEX_OK) { + return rc; + } + } + return sidex_json_write_array_end(writer); +} + +static int write_object_value(sidex_json_writer *writer, const sidex_json_object *object, + size_t max_depth) +{ + int rc = sidex_json_write_object_begin(writer); + + if (rc != SIDEX_OK) { + return rc; + } + for (size_t i = 0; i < object->len; i++) { + if (i > 0) { + rc = sidex_json_write_value_separator(writer); + if (rc != SIDEX_OK) { + return rc; + } + } + rc = sidex_json_write_object_key(writer, object->entries[i].key); + if (rc != SIDEX_OK) { + return rc; + } + rc = write_value_with_depth(writer, &object->entries[i].value, max_depth - 1u); + if (rc != SIDEX_OK) { + return rc; + } + } + return sidex_json_write_object_end(writer); +} + +static int write_value_with_depth(sidex_json_writer *writer, const sidex_json_value *value, + size_t max_depth) +{ + if (max_depth == 0) { + return SIDEX_ERR_DEPTH; + } + + switch (value->kind) { + case SIDEX_JSON_VALUE_NULL: + return sidex_json_write_null(writer); + case SIDEX_JSON_VALUE_BOOL: + return sidex_json_write_bool(writer, value->as.boolean); + case SIDEX_JSON_VALUE_NUMBER: + if (!number_lexeme_is_valid(value->as.number)) { + return SIDEX_ERR_SYNTAX; + } + return sidex_json_write_raw(writer, value->as.number.ptr, value->as.number.len); + case SIDEX_JSON_VALUE_STRING: + return sidex_json_write_str(writer, value->as.string); + case SIDEX_JSON_VALUE_ARRAY: + return write_array_value(writer, &value->as.array, max_depth); + case SIDEX_JSON_VALUE_OBJECT: + return write_object_value(writer, &value->as.object, max_depth); + default: + return SIDEX_ERR_EXPECTED; + } +} + +int sidex_json_write_value(sidex_json_writer *writer, const sidex_json_value *value, + size_t max_depth) +{ + return write_value_with_depth(writer, value, max_depth); +} diff --git a/crates/sidex-gen-c/runtime/sidex_json_value.h b/crates/sidex-gen-c/runtime/sidex_json_value.h new file mode 100644 index 0000000..b473973 --- /dev/null +++ b/crates/sidex-gen-c/runtime/sidex_json_value.h @@ -0,0 +1,82 @@ +#ifndef SIDEX_JSON_VALUE_H +#define SIDEX_JSON_VALUE_H + +#include "sidex_json_reader.h" +#include "sidex_json_writer.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct sidex_json_value sidex_json_value; +typedef struct sidex_json_entry sidex_json_entry; + +/** JSON value kind used by the arbitrary JSON representation. */ +typedef enum sidex_json_value_kind { + SIDEX_JSON_VALUE_NULL = 0, + SIDEX_JSON_VALUE_BOOL, + SIDEX_JSON_VALUE_NUMBER, + SIDEX_JSON_VALUE_STRING, + SIDEX_JSON_VALUE_ARRAY, + SIDEX_JSON_VALUE_OBJECT, +} sidex_json_value_kind; + +/** Array storage for arbitrary JSON values. */ +typedef struct sidex_json_array { + sidex_json_value *items; + size_t len; +} sidex_json_array; + +/** Object storage for arbitrary JSON values. */ +typedef struct sidex_json_object { + sidex_json_entry *entries; + size_t len; +} sidex_json_object; + +/** + * Arbitrary JSON value. + * + * Strings and numbers may borrow from the original JSON input. Strings with + * escapes are decoded into the arena. Arrays and objects are stored in the + * arena. The input buffer and arena must both outlive values decoded from them. + */ +struct sidex_json_value { + sidex_json_value_kind kind; + union { + bool boolean; + sidex_str number; + sidex_str string; + sidex_json_array array; + sidex_json_object object; + } as; +}; + +/** Object entry storage for arbitrary JSON values. */ +struct sidex_json_entry { + sidex_str key; + sidex_json_value value; +}; + +/** + * Decodes one JSON value into an arbitrary JSON representation. + * + * `max_depth` bounds recursive value nesting. The top-level value consumes one + * level, so a scalar requires at least 1 and `[0]` requires at least 2. + */ +int sidex_json_read_value(sidex_json_reader *reader, sidex_json_value *out, sidex_arena *arena, + size_t max_depth); + +/** + * Encodes one arbitrary JSON value. + * + * `max_depth` bounds recursive value nesting. The top-level value consumes one + * level, so a scalar requires at least 1 and `[0]` requires at least 2. + */ +int sidex_json_write_value(sidex_json_writer *writer, const sidex_json_value *value, + size_t max_depth); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/crates/sidex-gen-c/runtime/sidex_json_writer.c b/crates/sidex-gen-c/runtime/sidex_json_writer.c new file mode 100644 index 0000000..6bd8ebf --- /dev/null +++ b/crates/sidex-gen-c/runtime/sidex_json_writer.c @@ -0,0 +1,206 @@ +#include "sidex_json_writer.h" + +#include +#include +#include + +void sidex_json_buffer_init(sidex_json_buffer *buffer, uint8_t *ptr, size_t cap) +{ + buffer->ptr = ptr; + buffer->cap = cap; + buffer->len = 0; +} + +static int buffer_write(void *ctx, const uint8_t *ptr, size_t len) +{ + sidex_json_buffer *buffer = (sidex_json_buffer *)ctx; + + if (buffer->len > buffer->cap || len > buffer->cap - buffer->len) { + return SIDEX_ERR_NO_SPACE; + } + if (len > 0) { + memcpy(buffer->ptr + buffer->len, ptr, len); + buffer->len += len; + } + return SIDEX_OK; +} + +sidex_json_writer sidex_json_writer_for_buffer(sidex_json_buffer *buffer) +{ + sidex_json_writer writer; + + writer.write = buffer_write; + writer.ctx = buffer; + return writer; +} + +int sidex_json_write_raw(sidex_json_writer *writer, const uint8_t *ptr, size_t len) +{ + int rc; + + if (len == 0) { + return SIDEX_OK; + } + rc = writer->write(writer->ctx, ptr, len); + if (rc == SIDEX_OK) { + return SIDEX_OK; + } + if (rc < 0) { + return rc; + } + return SIDEX_ERR_CALLBACK; +} + +static int write_cstr(sidex_json_writer *writer, const char *str) +{ + return sidex_json_write_raw(writer, (const uint8_t *)str, strlen(str)); +} + +int sidex_json_write_null(sidex_json_writer *writer) +{ + return write_cstr(writer, "null"); +} + +int sidex_json_write_bool(sidex_json_writer *writer, bool value) +{ + return write_cstr(writer, value ? "true" : "false"); +} + +int sidex_json_write_i64(sidex_json_writer *writer, int64_t value) +{ + char buffer[32]; + int len = snprintf(buffer, sizeof(buffer), "%" PRId64, value); + + if (len < 0 || (size_t)len >= sizeof(buffer)) { + return SIDEX_ERR_OVERFLOW; + } + return sidex_json_write_raw(writer, (const uint8_t *)buffer, (size_t)len); +} + +int sidex_json_write_u64(sidex_json_writer *writer, uint64_t value) +{ + char buffer[32]; + int len = snprintf(buffer, sizeof(buffer), "%" PRIu64, value); + + if (len < 0 || (size_t)len >= sizeof(buffer)) { + return SIDEX_ERR_OVERFLOW; + } + return sidex_json_write_raw(writer, (const uint8_t *)buffer, (size_t)len); +} + +static int write_hex_escape(sidex_json_writer *writer, uint8_t byte) +{ + static const uint8_t hex[] = "0123456789abcdef"; + uint8_t escaped[6]; + + escaped[0] = '\\'; + escaped[1] = 'u'; + escaped[2] = '0'; + escaped[3] = '0'; + escaped[4] = hex[(byte >> 4) & 0x0fu]; + escaped[5] = hex[byte & 0x0fu]; + return sidex_json_write_raw(writer, escaped, sizeof(escaped)); +} + +static int write_escape_pair(sidex_json_writer *writer, uint8_t escaped) +{ + uint8_t pair[2]; + + pair[0] = '\\'; + pair[1] = escaped; + return sidex_json_write_raw(writer, pair, sizeof(pair)); +} + +int sidex_json_write_str(sidex_json_writer *writer, sidex_str value) +{ + int rc; + + if (!sidex_utf8_validate(value.ptr, value.len)) { + return SIDEX_ERR_UTF8; + } + + rc = write_cstr(writer, "\""); + if (rc != SIDEX_OK) { + return rc; + } + + for (size_t i = 0; i < value.len; i++) { + uint8_t ch = value.ptr[i]; + + switch (ch) { + case '"': + rc = write_escape_pair(writer, '"'); + break; + case '\\': + rc = write_escape_pair(writer, '\\'); + break; + case '\b': + rc = write_escape_pair(writer, 'b'); + break; + case '\f': + rc = write_escape_pair(writer, 'f'); + break; + case '\n': + rc = write_escape_pair(writer, 'n'); + break; + case '\r': + rc = write_escape_pair(writer, 'r'); + break; + case '\t': + rc = write_escape_pair(writer, 't'); + break; + default: + if (ch < 0x20u) { + rc = write_hex_escape(writer, ch); + } else { + rc = sidex_json_write_raw(writer, &ch, 1); + } + break; + } + if (rc != SIDEX_OK) { + return rc; + } + } + + return write_cstr(writer, "\""); +} + +int sidex_json_write_object_begin(sidex_json_writer *writer) +{ + return write_cstr(writer, "{"); +} + +int sidex_json_write_object_end(sidex_json_writer *writer) +{ + return write_cstr(writer, "}"); +} + +int sidex_json_write_array_begin(sidex_json_writer *writer) +{ + return write_cstr(writer, "["); +} + +int sidex_json_write_array_end(sidex_json_writer *writer) +{ + return write_cstr(writer, "]"); +} + +int sidex_json_write_name_separator(sidex_json_writer *writer) +{ + return write_cstr(writer, ":"); +} + +int sidex_json_write_value_separator(sidex_json_writer *writer) +{ + return write_cstr(writer, ","); +} + +int sidex_json_write_object_key(sidex_json_writer *writer, sidex_str key) +{ + int rc = sidex_json_write_str(writer, key); + + if (rc != SIDEX_OK) { + return rc; + } + return sidex_json_write_name_separator(writer); +} diff --git a/crates/sidex-gen-c/runtime/sidex_json_writer.h b/crates/sidex-gen-c/runtime/sidex_json_writer.h new file mode 100644 index 0000000..1e11ee6 --- /dev/null +++ b/crates/sidex-gen-c/runtime/sidex_json_writer.h @@ -0,0 +1,75 @@ +#ifndef SIDEX_JSON_WRITER_H +#define SIDEX_JSON_WRITER_H + +#include "sidex_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** Callback used by the streaming JSON writer to emit bytes. */ +typedef int (*sidex_json_write_fn)(void *ctx, const uint8_t *ptr, size_t len); + +/** Streaming JSON writer backed by a caller-provided callback. */ +typedef struct sidex_json_writer { + sidex_json_write_fn write; + void *ctx; +} sidex_json_writer; + +/** Fixed-size output buffer adapter for sidex_json_writer. */ +typedef struct sidex_json_buffer { + uint8_t *ptr; + size_t cap; + size_t len; +} sidex_json_buffer; + +/** Initializes a fixed-size output buffer. */ +void sidex_json_buffer_init(sidex_json_buffer *buffer, uint8_t *ptr, size_t cap); + +/** Creates a writer that appends to a fixed-size output buffer. */ +sidex_json_writer sidex_json_writer_for_buffer(sidex_json_buffer *buffer); + +/** Emits raw bytes through the writer callback. */ +int sidex_json_write_raw(sidex_json_writer *writer, const uint8_t *ptr, size_t len); + +/** Writes a JSON null value. */ +int sidex_json_write_null(sidex_json_writer *writer); + +/** Writes a JSON boolean value. */ +int sidex_json_write_bool(sidex_json_writer *writer, bool value); + +/** Writes a JSON signed integer value. */ +int sidex_json_write_i64(sidex_json_writer *writer, int64_t value); + +/** Writes a JSON unsigned integer value. */ +int sidex_json_write_u64(sidex_json_writer *writer, uint64_t value); + +/** Writes a JSON string, validating UTF-8 and escaping JSON control bytes. */ +int sidex_json_write_str(sidex_json_writer *writer, sidex_str value); + +/** Writes `{`. */ +int sidex_json_write_object_begin(sidex_json_writer *writer); + +/** Writes `}`. */ +int sidex_json_write_object_end(sidex_json_writer *writer); + +/** Writes `[`. */ +int sidex_json_write_array_begin(sidex_json_writer *writer); + +/** Writes `]`. */ +int sidex_json_write_array_end(sidex_json_writer *writer); + +/** Writes `:`. */ +int sidex_json_write_name_separator(sidex_json_writer *writer); + +/** Writes `,`. */ +int sidex_json_write_value_separator(sidex_json_writer *writer); + +/** Writes a JSON object key followed by `:`. */ +int sidex_json_write_object_key(sidex_json_writer *writer, sidex_str key); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/crates/sidex-gen-c/runtime/sidex_types.c b/crates/sidex-gen-c/runtime/sidex_types.c new file mode 100644 index 0000000..10a49d4 --- /dev/null +++ b/crates/sidex-gen-c/runtime/sidex_types.c @@ -0,0 +1,125 @@ +#include "sidex_types.h" + +static bool sidex_is_power_of_two(size_t value) +{ + return value != 0 && (value & (value - 1)) == 0; +} + +void *sidex_arena_alloc(sidex_arena *arena, size_t size, size_t align) +{ + uintptr_t base; + uintptr_t current; + uintptr_t aligned; + size_t padding; + + if (arena == NULL || arena->ptr == NULL || !sidex_is_power_of_two(align)) { + return NULL; + } + if (arena->len > arena->cap) { + return NULL; + } + + base = (uintptr_t)arena->ptr; + current = base + arena->len; + aligned = (current + (uintptr_t)align - 1u) & ~((uintptr_t)align - 1u); + padding = (size_t)(aligned - current); + + if (padding > arena->cap - arena->len || size > arena->cap - arena->len - padding) { + return NULL; + } + + arena->len += padding + size; + return (void *)aligned; +} + +void *sidex_arena_alloc_array(sidex_arena *arena, size_t count, size_t elem_size, size_t align) +{ + if (elem_size != 0 && count > (SIZE_MAX / elem_size)) { + return NULL; + } + return sidex_arena_alloc(arena, count * elem_size, align); +} + +static bool sidex_utf8_is_continuation(uint8_t ch) +{ + return (ch & 0xc0u) == 0x80u; +} + +bool sidex_utf8_validate(const uint8_t *ptr, size_t len) +{ + size_t i = 0; + + /* + * Implements the valid UTF-8 byte ranges from RFC 3629 section 4. + * The special E0/ED/F0/F4 cases reject overlong encodings, surrogate + * code points, and code points above U+10FFFF. + */ + while (i < len) { + uint8_t b0 = ptr[i]; + + if (b0 <= 0x7fu) { + i++; + continue; + } + if (b0 >= 0xc2u && b0 <= 0xdfu) { + if (i + 1 >= len || !sidex_utf8_is_continuation(ptr[i + 1])) { + return false; + } + i += 2; + continue; + } + if (b0 == 0xe0u) { + if (i + 2 >= len || ptr[i + 1] < 0xa0u || ptr[i + 1] > 0xbfu || + !sidex_utf8_is_continuation(ptr[i + 2])) { + return false; + } + i += 3; + continue; + } + if ((b0 >= 0xe1u && b0 <= 0xecu) || (b0 >= 0xeeu && b0 <= 0xefu)) { + if (i + 2 >= len || !sidex_utf8_is_continuation(ptr[i + 1]) || + !sidex_utf8_is_continuation(ptr[i + 2])) { + return false; + } + i += 3; + continue; + } + if (b0 == 0xedu) { + if (i + 2 >= len || ptr[i + 1] < 0x80u || ptr[i + 1] > 0x9fu || + !sidex_utf8_is_continuation(ptr[i + 2])) { + return false; + } + i += 3; + continue; + } + if (b0 == 0xf0u) { + if (i + 3 >= len || ptr[i + 1] < 0x90u || ptr[i + 1] > 0xbfu || + !sidex_utf8_is_continuation(ptr[i + 2]) || + !sidex_utf8_is_continuation(ptr[i + 3])) { + return false; + } + i += 4; + continue; + } + if (b0 >= 0xf1u && b0 <= 0xf3u) { + if (i + 3 >= len || !sidex_utf8_is_continuation(ptr[i + 1]) || + !sidex_utf8_is_continuation(ptr[i + 2]) || + !sidex_utf8_is_continuation(ptr[i + 3])) { + return false; + } + i += 4; + continue; + } + if (b0 == 0xf4u) { + if (i + 3 >= len || ptr[i + 1] < 0x80u || ptr[i + 1] > 0x8fu || + !sidex_utf8_is_continuation(ptr[i + 2]) || + !sidex_utf8_is_continuation(ptr[i + 3])) { + return false; + } + i += 4; + continue; + } + return false; + } + return true; +} diff --git a/crates/sidex-gen-c/runtime/sidex_types.h b/crates/sidex-gen-c/runtime/sidex_types.h new file mode 100644 index 0000000..ccb263c --- /dev/null +++ b/crates/sidex-gen-c/runtime/sidex_types.h @@ -0,0 +1,111 @@ +#ifndef SIDEX_TYPES_H +#define SIDEX_TYPES_H + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define SIDEX_ALIGNOF(type) \ + offsetof( \ + struct { \ + char sidex_padding; \ + type sidex_value; \ + }, \ + sidex_value) + +/** Common status codes returned by the Sidex C runtime. */ +typedef enum sidex_status { + SIDEX_OK = 0, + SIDEX_ERR_EOF = -1, + SIDEX_ERR_SYNTAX = -2, + SIDEX_ERR_EXPECTED = -3, + SIDEX_ERR_OVERFLOW = -4, + SIDEX_ERR_NO_SPACE = -5, + SIDEX_ERR_UTF8 = -6, + SIDEX_ERR_UNSUPPORTED = -7, + SIDEX_ERR_CALLBACK = -8, + SIDEX_ERR_DEPTH = -9, +} sidex_status; + +/** Borrowed UTF-8 string. The bytes are not NUL terminated. */ +typedef struct sidex_str { + const uint8_t *ptr; + size_t len; +} sidex_str; + +/** + * Caller-owned bump arena used by decoders when data cannot be borrowed. + * + * Decoded values may borrow from this storage. Reset it only after those + * values are no longer used. + */ +typedef struct sidex_arena { + uint8_t *ptr; + size_t cap; + size_t len; +} sidex_arena; + +/** Creates a borrowed string from raw bytes. The bytes must be valid UTF-8. */ +static inline sidex_str sidex_str_from_parts(const uint8_t *ptr, size_t len) +{ + sidex_str str; + + str.ptr = ptr; + str.len = len; + return str; +} + +/** Creates a borrowed string from a NUL-terminated UTF-8 C string. */ +static inline sidex_str sidex_str_from_cstr(const char *ptr) +{ + return sidex_str_from_parts((const uint8_t *)ptr, strlen(ptr)); +} + +/** Compares two borrowed strings byte-for-byte. */ +static inline bool sidex_str_eq(sidex_str lhs, sidex_str rhs) +{ + if (lhs.len != rhs.len) { + return false; + } + if (lhs.len == 0) { + return true; + } + return memcmp(lhs.ptr, rhs.ptr, lhs.len) == 0; +} + +/** Creates an empty arena from a caller-owned byte buffer. */ +static inline sidex_arena sidex_arena_from_parts(uint8_t *ptr, size_t cap) +{ + sidex_arena arena; + + arena.ptr = ptr; + arena.cap = cap; + arena.len = 0; + return arena; +} + +/** Resets an arena, invalidating decoded values that borrow from it. */ +static inline void sidex_arena_reset(sidex_arena *arena) +{ + arena->len = 0; +} + +/** Allocates aligned memory from an arena. */ +void *sidex_arena_alloc(sidex_arena *arena, size_t size, size_t align); + +/** Allocates an aligned array from an arena, checking for size overflow. */ +void *sidex_arena_alloc_array(sidex_arena *arena, size_t count, size_t elem_size, size_t align); + +/** Returns true if the byte slice is well-formed UTF-8. */ +bool sidex_utf8_validate(const uint8_t *ptr, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/crates/sidex-gen-c/src/lib.rs b/crates/sidex-gen-c/src/lib.rs new file mode 100644 index 0000000..172fa24 --- /dev/null +++ b/crates/sidex-gen-c/src/lib.rs @@ -0,0 +1,17 @@ +#![doc = include_str!("../README.md")] + +use sidex_gen::Generator; +use sidex_gen::diagnostics; + +/// Implements [`Generator`] for C. +/// +/// The backend is still under construction. The portable runtime lives in +/// `runtime/` and is tested independently before generated code starts using it. +#[derive(Clone, Debug, Default)] +pub struct CGenerator; + +impl Generator for CGenerator { + fn generate(&self, _job: sidex_gen::Job) -> diagnostics::Result<()> { + todo!("C code generation is not implemented yet") + } +} diff --git a/crates/sidex-gen-c/tests/runtime.rs b/crates/sidex-gen-c/tests/runtime.rs new file mode 100644 index 0000000..e4ddc95 --- /dev/null +++ b/crates/sidex-gen-c/tests/runtime.rs @@ -0,0 +1,62 @@ +use std::env; +use std::fs; +use std::path::Path; +use std::path::PathBuf; +use std::process::Command; + +#[test] +fn c_json_runtime_builds_and_passes_tests() { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let workspace_dir = manifest_dir + .parent() + .and_then(Path::parent) + .expect("crate must live under workspace/crates"); + let out_dir = workspace_dir.join("target").join("sidex-gen-c-tests"); + fs::create_dir_all(&out_dir).expect("failed to create C test output directory"); + + let exe = out_dir.join(format!("runtime_json_test{}", env::consts::EXE_SUFFIX)); + let cc = env::var_os("CC").unwrap_or_else(|| "cc".into()); + + let runtime_dir = manifest_dir.join("runtime"); + let test_src = manifest_dir.join("tests").join("runtime_json.c"); + let sources = [ + runtime_dir.join("sidex_types.c"), + runtime_dir.join("sidex_json_reader.c"), + runtime_dir.join("sidex_json_value.c"), + runtime_dir.join("sidex_json_writer.c"), + test_src, + ]; + + let compile = Command::new(&cc) + .arg("-std=c99") + .arg("-Wall") + .arg("-Wextra") + .arg("-Werror") + .arg("-I") + .arg(&runtime_dir) + .args(sources) + .arg("-o") + .arg(&exe) + .output() + .expect("failed to run C compiler"); + + assert!( + compile.status.success(), + "C runtime test compilation failed with status {:?}\nstdout:\n{}\nstderr:\n{}", + compile.status.code(), + String::from_utf8_lossy(&compile.stdout), + String::from_utf8_lossy(&compile.stderr) + ); + + let run = Command::new(&exe) + .output() + .expect("failed to run compiled C runtime test"); + + assert!( + run.status.success(), + "C runtime test failed with status {:?}\nstdout:\n{}\nstderr:\n{}", + run.status.code(), + String::from_utf8_lossy(&run.stdout), + String::from_utf8_lossy(&run.stderr) + ); +} diff --git a/crates/sidex-gen-c/tests/runtime_json.c b/crates/sidex-gen-c/tests/runtime_json.c new file mode 100644 index 0000000..1f75eaa --- /dev/null +++ b/crates/sidex-gen-c/tests/runtime_json.c @@ -0,0 +1,424 @@ +#include "sidex_json.h" +#include "sidex_json_reader.h" +#include "sidex_json_writer.h" + +#include +#include + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + return __LINE__; \ + } \ + } while (0) + +#define CHECK_OK(expression) \ + do { \ + int rc_ = (expression); \ + if (rc_ != SIDEX_OK) { \ + return __LINE__; \ + } \ + } while (0) + +#define CHECK_RC(expression, expected) \ + do { \ + int rc_ = (expression); \ + if (rc_ != (expected)) { \ + return __LINE__; \ + } \ + } while (0) + +static bool bytes_eq(sidex_str actual, const uint8_t *expected, size_t expected_len) +{ + if (actual.len != expected_len) { + return false; + } + if (expected_len == 0) { + return true; + } + return memcmp(actual.ptr, expected, expected_len) == 0; +} + +static bool str_eq_cstr(sidex_str actual, const char *expected) +{ + return bytes_eq(actual, (const uint8_t *)expected, strlen(expected)); +} + +static bool ptr_in_range(const uint8_t *ptr, const uint8_t *start, size_t len) +{ + uintptr_t address = (uintptr_t)ptr; + uintptr_t range_start = (uintptr_t)start; + + return address >= range_start && address - range_start < len; +} + +static int read_key(sidex_json_reader *reader, const char *expected) +{ + uint8_t arena_storage[32]; + sidex_arena arena = sidex_arena_from_parts(arena_storage, sizeof(arena_storage)); + sidex_str key; + + CHECK_OK(sidex_json_read_str(reader, &key, &arena)); + CHECK(str_eq_cstr(key, expected)); + CHECK_OK(sidex_json_expect(reader, SIDEX_JSON_TOKEN_NAME_SEPARATOR)); + return 0; +} + +static int test_utf8(void) +{ + static const uint8_t ascii[] = {'a'}; + static const uint8_t min_2[] = {0xc2u, 0x80u}; + static const uint8_t max_2[] = {0xdfu, 0xbfu}; + static const uint8_t min_3[] = {0xe0u, 0xa0u, 0x80u}; + static const uint8_t before_surrogate[] = {0xedu, 0x9fu, 0xbfu}; + static const uint8_t after_surrogate[] = {0xeeu, 0x80u, 0x80u}; + static const uint8_t min_4[] = {0xf0u, 0x90u, 0x80u, 0x80u}; + static const uint8_t max_4[] = {0xf4u, 0x8fu, 0xbfu, 0xbfu}; + static const uint8_t lone_continuation[] = {0x80u}; + static const uint8_t overlong[] = {0xc0u, 0x80u}; + static const uint8_t overlong_3[] = {0xe0u, 0x80u, 0x80u}; + static const uint8_t surrogate[] = {0xedu, 0xa0u, 0x80u}; + static const uint8_t overlong_4[] = {0xf0u, 0x80u, 0x80u, 0x80u}; + static const uint8_t above_max[] = {0xf4u, 0x90u, 0x80u, 0x80u}; + static const uint8_t invalid_starter[] = {0xf5u, 0x80u, 0x80u, 0x80u}; + static const uint8_t truncated[] = {0xe2u, 0x82u}; + + CHECK(sidex_utf8_validate(ascii, sizeof(ascii))); + CHECK(sidex_utf8_validate(min_2, sizeof(min_2))); + CHECK(sidex_utf8_validate(max_2, sizeof(max_2))); + CHECK(sidex_utf8_validate(min_3, sizeof(min_3))); + CHECK(sidex_utf8_validate(before_surrogate, sizeof(before_surrogate))); + CHECK(sidex_utf8_validate(after_surrogate, sizeof(after_surrogate))); + CHECK(sidex_utf8_validate(min_4, sizeof(min_4))); + CHECK(sidex_utf8_validate(max_4, sizeof(max_4))); + CHECK(!sidex_utf8_validate(lone_continuation, sizeof(lone_continuation))); + CHECK(!sidex_utf8_validate(overlong, sizeof(overlong))); + CHECK(!sidex_utf8_validate(overlong_3, sizeof(overlong_3))); + CHECK(!sidex_utf8_validate(surrogate, sizeof(surrogate))); + CHECK(!sidex_utf8_validate(overlong_4, sizeof(overlong_4))); + CHECK(!sidex_utf8_validate(above_max, sizeof(above_max))); + CHECK(!sidex_utf8_validate(invalid_starter, sizeof(invalid_starter))); + CHECK(!sidex_utf8_validate(truncated, sizeof(truncated))); + return 0; +} + +static int test_writer(void) +{ + static const uint8_t zero[] = {'a', 0, 'b'}; + static const char expected[] = + "{\"name\":\"Sidex\",\"line\":\"a\\nb\",\"zero\":\"a\\u0000b\"," + "\"ok\":true,\"n\":-42,\"u\":42}"; + uint8_t storage[160]; + sidex_json_buffer buffer; + sidex_json_writer writer; + uint8_t small_storage[4]; + sidex_json_buffer small_buffer; + sidex_json_writer small_writer; + static const uint8_t invalid_utf8[] = {0xc0u, 0x80u}; + + sidex_json_buffer_init(&buffer, storage, sizeof(storage)); + writer = sidex_json_writer_for_buffer(&buffer); + + CHECK_OK(sidex_json_write_object_begin(&writer)); + CHECK_OK(sidex_json_write_object_key(&writer, sidex_str_from_cstr("name"))); + CHECK_OK(sidex_json_write_str(&writer, sidex_str_from_cstr("Sidex"))); + CHECK_OK(sidex_json_write_value_separator(&writer)); + CHECK_OK(sidex_json_write_object_key(&writer, sidex_str_from_cstr("line"))); + CHECK_OK(sidex_json_write_str(&writer, sidex_str_from_cstr("a\nb"))); + CHECK_OK(sidex_json_write_value_separator(&writer)); + CHECK_OK(sidex_json_write_object_key(&writer, sidex_str_from_cstr("zero"))); + CHECK_OK(sidex_json_write_str(&writer, sidex_str_from_parts(zero, sizeof(zero)))); + CHECK_OK(sidex_json_write_value_separator(&writer)); + CHECK_OK(sidex_json_write_object_key(&writer, sidex_str_from_cstr("ok"))); + CHECK_OK(sidex_json_write_bool(&writer, true)); + CHECK_OK(sidex_json_write_value_separator(&writer)); + CHECK_OK(sidex_json_write_object_key(&writer, sidex_str_from_cstr("n"))); + CHECK_OK(sidex_json_write_i64(&writer, -42)); + CHECK_OK(sidex_json_write_value_separator(&writer)); + CHECK_OK(sidex_json_write_object_key(&writer, sidex_str_from_cstr("u"))); + CHECK_OK(sidex_json_write_u64(&writer, 42)); + CHECK_OK(sidex_json_write_object_end(&writer)); + + CHECK(buffer.len == strlen(expected)); + CHECK(memcmp(storage, expected, buffer.len) == 0); + + sidex_json_buffer_init(&small_buffer, small_storage, sizeof(small_storage)); + small_writer = sidex_json_writer_for_buffer(&small_buffer); + CHECK_RC(sidex_json_write_str(&small_writer, sidex_str_from_cstr("abcd")), + SIDEX_ERR_NO_SPACE); + CHECK_RC( + sidex_json_write_str(&writer, sidex_str_from_parts(invalid_utf8, sizeof(invalid_utf8))), + SIDEX_ERR_UTF8); + return 0; +} + +static int test_reader(void) +{ + static const char input[] = + "{\"name\":\"Sidex\",\"line\":\"a\\nb\"," + "\"unicode\":\"\\u00df\\ud83d\\ude80\",\"zero\":\"a\\u0000b\"," + "\"n\":-42,\"u\":42,\"ok\":true," + "\"ignored\":{\"x\":[1,2.5,null,\"long escaped \\u00df string\"]}}"; + static const uint8_t unicode_expected[] = {0xc3u, 0x9fu, 0xf0u, 0x9fu, 0x9au, 0x80u}; + static const uint8_t zero_expected[] = {'a', 0, 'b'}; + sidex_json_reader reader; + sidex_json_token_kind kind; + uint8_t arena_storage[64]; + sidex_arena arena = sidex_arena_from_parts(arena_storage, sizeof(arena_storage)); + sidex_str value; + int64_t n; + uint64_t u; + bool ok; + + sidex_json_reader_init(&reader, (const uint8_t *)input, strlen(input)); + CHECK_OK(sidex_json_expect(&reader, SIDEX_JSON_TOKEN_OBJECT_BEGIN)); + + CHECK_OK(read_key(&reader, "name")); + CHECK_OK(sidex_json_read_str(&reader, &value, &arena)); + CHECK(str_eq_cstr(value, "Sidex")); + CHECK_OK(sidex_json_expect(&reader, SIDEX_JSON_TOKEN_VALUE_SEPARATOR)); + + CHECK_OK(read_key(&reader, "line")); + CHECK_OK(sidex_json_read_str(&reader, &value, &arena)); + CHECK(str_eq_cstr(value, "a\nb")); + CHECK_OK(sidex_json_expect(&reader, SIDEX_JSON_TOKEN_VALUE_SEPARATOR)); + + CHECK_OK(read_key(&reader, "unicode")); + CHECK_OK(sidex_json_read_str(&reader, &value, &arena)); + CHECK(bytes_eq(value, unicode_expected, sizeof(unicode_expected))); + CHECK_OK(sidex_json_expect(&reader, SIDEX_JSON_TOKEN_VALUE_SEPARATOR)); + + CHECK_OK(read_key(&reader, "zero")); + CHECK_OK(sidex_json_read_str(&reader, &value, &arena)); + CHECK(bytes_eq(value, zero_expected, sizeof(zero_expected))); + CHECK_OK(sidex_json_expect(&reader, SIDEX_JSON_TOKEN_VALUE_SEPARATOR)); + + CHECK_OK(read_key(&reader, "n")); + CHECK_OK(sidex_json_read_i64(&reader, &n)); + CHECK(n == -42); + CHECK_OK(sidex_json_expect(&reader, SIDEX_JSON_TOKEN_VALUE_SEPARATOR)); + + CHECK_OK(read_key(&reader, "u")); + CHECK_OK(sidex_json_read_u64(&reader, &u)); + CHECK(u == 42); + CHECK_OK(sidex_json_expect(&reader, SIDEX_JSON_TOKEN_VALUE_SEPARATOR)); + + CHECK_OK(read_key(&reader, "ok")); + CHECK_OK(sidex_json_read_bool(&reader, &ok)); + CHECK(ok); + CHECK_OK(sidex_json_expect(&reader, SIDEX_JSON_TOKEN_VALUE_SEPARATOR)); + + CHECK_OK(read_key(&reader, "ignored")); + CHECK_OK(sidex_json_skip_value(&reader, SIDEX_JSON_DEFAULT_MAX_DEPTH)); + CHECK_OK(sidex_json_expect(&reader, SIDEX_JSON_TOKEN_OBJECT_END)); + CHECK_OK(sidex_json_peek(&reader, &kind)); + CHECK(kind == SIDEX_JSON_TOKEN_END); + return 0; +} + +static int test_reader_errors(void) +{ + static const uint8_t invalid_utf8_string[] = {'"', 0xc0u, 0x80u, '"'}; + static const char fractional_integer[] = "1.25"; + static const char bad_surrogate[] = "\"\\ud83d\""; + sidex_json_reader reader; + uint8_t arena_storage[16]; + sidex_arena arena = sidex_arena_from_parts(arena_storage, sizeof(arena_storage)); + sidex_str value; + int64_t number; + + sidex_json_reader_init(&reader, invalid_utf8_string, sizeof(invalid_utf8_string)); + CHECK_RC(sidex_json_read_str(&reader, &value, &arena), SIDEX_ERR_UTF8); + + sidex_json_reader_init(&reader, (const uint8_t *)fractional_integer, + strlen(fractional_integer)); + CHECK_RC(sidex_json_read_i64(&reader, &number), SIDEX_ERR_UNSUPPORTED); + + sidex_json_reader_init(&reader, (const uint8_t *)bad_surrogate, strlen(bad_surrogate)); + CHECK_RC(sidex_json_skip_value(&reader, SIDEX_JSON_DEFAULT_MAX_DEPTH), SIDEX_ERR_UTF8); + return 0; +} + +static int test_arena(void) +{ + static const char input[] = "[\"a\\nb\",\"c\\td\"]"; + sidex_json_reader reader; + uint8_t arena_storage[16]; + sidex_arena arena = sidex_arena_from_parts(arena_storage, sizeof(arena_storage)); + sidex_str first; + sidex_str second; + + sidex_json_reader_init(&reader, (const uint8_t *)input, strlen(input)); + CHECK_OK(sidex_json_expect(&reader, SIDEX_JSON_TOKEN_ARRAY_BEGIN)); + CHECK_OK(sidex_json_read_str(&reader, &first, &arena)); + CHECK(first.ptr == arena_storage); + CHECK(first.len == 3); + CHECK_OK(sidex_json_expect(&reader, SIDEX_JSON_TOKEN_VALUE_SEPARATOR)); + CHECK_OK(sidex_json_read_str(&reader, &second, &arena)); + CHECK(second.ptr == arena_storage + first.len); + CHECK(second.len == 3); + CHECK(arena.len == first.len + second.len); + CHECK(str_eq_cstr(first, "a\nb")); + CHECK(str_eq_cstr(second, "c\td")); + CHECK_OK(sidex_json_expect(&reader, SIDEX_JSON_TOKEN_ARRAY_END)); + + sidex_arena_reset(&arena); + CHECK(arena.len == 0); + return 0; +} + +static int test_json_value(void) +{ + static const char input[] = + "{\"a\":[true,null,{\"b\":\"x\\ny\"}],\"n\":-12.5e+2,\"s\":\"\\u00df\"}"; + static const char expected[] = + "{\"a\":[true,null,{\"b\":\"x\\ny\"}],\"n\":-12.5e+2,\"s\":\"\xc3\x9f\"}"; + uint8_t arena_storage[512]; + sidex_arena arena = sidex_arena_from_parts(arena_storage, sizeof(arena_storage)); + sidex_json_reader reader; + sidex_json_value value; + sidex_json_buffer buffer; + sidex_json_writer writer; + sidex_json_token_kind kind; + uint8_t output[160]; + + sidex_json_reader_init(&reader, (const uint8_t *)input, strlen(input)); + CHECK_OK(sidex_json_read_value(&reader, &value, &arena, SIDEX_JSON_DEFAULT_MAX_DEPTH)); + CHECK(value.kind == SIDEX_JSON_VALUE_OBJECT); + CHECK(value.as.object.len == 3); + CHECK(str_eq_cstr(value.as.object.entries[0].key, "a")); + CHECK(value.as.object.entries[0].value.kind == SIDEX_JSON_VALUE_ARRAY); + CHECK(value.as.object.entries[0].value.as.array.len == 3); + CHECK(value.as.object.entries[0].value.as.array.items[0].kind == SIDEX_JSON_VALUE_BOOL); + CHECK(value.as.object.entries[0].value.as.array.items[0].as.boolean); + CHECK(value.as.object.entries[0].value.as.array.items[1].kind == SIDEX_JSON_VALUE_NULL); + CHECK(value.as.object.entries[0].value.as.array.items[2].kind == SIDEX_JSON_VALUE_OBJECT); + CHECK(str_eq_cstr(value.as.object.entries[1].key, "n")); + CHECK(value.as.object.entries[1].value.kind == SIDEX_JSON_VALUE_NUMBER); + CHECK(str_eq_cstr(value.as.object.entries[1].value.as.number, "-12.5e+2")); + CHECK(str_eq_cstr(value.as.object.entries[2].key, "s")); + CHECK(value.as.object.entries[2].value.kind == SIDEX_JSON_VALUE_STRING); + CHECK(bytes_eq(value.as.object.entries[2].value.as.string, (const uint8_t *)"\xc3\x9f", 2)); + CHECK_OK(sidex_json_peek(&reader, &kind)); + CHECK(kind == SIDEX_JSON_TOKEN_END); + + sidex_json_buffer_init(&buffer, output, sizeof(output)); + writer = sidex_json_writer_for_buffer(&buffer); + CHECK_OK(sidex_json_write_value(&writer, &value, SIDEX_JSON_DEFAULT_MAX_DEPTH)); + CHECK(buffer.len == strlen(expected)); + CHECK(memcmp(output, expected, buffer.len) == 0); + return 0; +} + +static int test_json_value_borrowing(void) +{ + static const char input[] = "{\"plain\":\"text\",\"escaped\":\"a\\nb\",\"number\":123}"; + const uint8_t *input_ptr = (const uint8_t *)input; + size_t input_len = strlen(input); + uint8_t arena_storage[128]; + sidex_arena arena = sidex_arena_from_parts(arena_storage, sizeof(arena_storage)); + sidex_json_reader reader; + sidex_json_value value; + + sidex_json_reader_init(&reader, input_ptr, input_len); + CHECK_OK(sidex_json_read_value(&reader, &value, &arena, SIDEX_JSON_DEFAULT_MAX_DEPTH)); + CHECK(value.kind == SIDEX_JSON_VALUE_OBJECT); + CHECK(value.as.object.len == 3); + CHECK(ptr_in_range(value.as.object.entries[0].key.ptr, input_ptr, input_len)); + CHECK(ptr_in_range(value.as.object.entries[0].value.as.string.ptr, input_ptr, input_len)); + CHECK(ptr_in_range(value.as.object.entries[1].value.as.string.ptr, arena_storage, + sizeof(arena_storage))); + CHECK(!ptr_in_range(value.as.object.entries[1].value.as.string.ptr, input_ptr, input_len)); + CHECK(ptr_in_range(value.as.object.entries[2].value.as.number.ptr, input_ptr, input_len)); + return 0; +} + +static int test_depth_limits(void) +{ + static const char nested[] = "[0]"; + static const sidex_json_value number = { + SIDEX_JSON_VALUE_NUMBER, + {.number = {(const uint8_t *)"0", 1}}, + }; + sidex_json_value items[1]; + sidex_json_value array; + uint8_t arena_storage[64]; + uint8_t output[8]; + sidex_arena arena = sidex_arena_from_parts(arena_storage, sizeof(arena_storage)); + sidex_json_reader reader; + sidex_json_buffer buffer; + sidex_json_writer writer; + + sidex_json_reader_init(&reader, (const uint8_t *)nested, strlen(nested)); + CHECK_RC(sidex_json_skip_value(&reader, 1), SIDEX_ERR_DEPTH); + sidex_json_reader_init(&reader, (const uint8_t *)nested, strlen(nested)); + CHECK_OK(sidex_json_skip_value(&reader, 2)); + + sidex_json_reader_init(&reader, (const uint8_t *)nested, strlen(nested)); + CHECK_RC(sidex_json_read_value(&reader, &array, &arena, 1), SIDEX_ERR_DEPTH); + sidex_json_reader_init(&reader, (const uint8_t *)nested, strlen(nested)); + CHECK_OK(sidex_json_read_value(&reader, &array, &arena, 2)); + + items[0] = number; + array.kind = SIDEX_JSON_VALUE_ARRAY; + array.as.array.items = items; + array.as.array.len = 1; + + sidex_json_buffer_init(&buffer, output, sizeof(output)); + writer = sidex_json_writer_for_buffer(&buffer); + CHECK_RC(sidex_json_write_value(&writer, &array, 1), SIDEX_ERR_DEPTH); + + sidex_json_buffer_init(&buffer, output, sizeof(output)); + writer = sidex_json_writer_for_buffer(&buffer); + CHECK_OK(sidex_json_write_value(&writer, &array, 2)); + CHECK(buffer.len == 3); + CHECK(memcmp(output, nested, buffer.len) == 0); + return 0; +} + +int main(void) +{ + int rc; + + rc = test_utf8(); + if (rc != 0) { + fprintf(stderr, "test_utf8 failed at line %d\n", rc); + return rc; + } + rc = test_writer(); + if (rc != 0) { + fprintf(stderr, "test_writer failed at line %d\n", rc); + return rc; + } + rc = test_reader(); + if (rc != 0) { + fprintf(stderr, "test_reader failed at line %d\n", rc); + return rc; + } + rc = test_reader_errors(); + if (rc != 0) { + fprintf(stderr, "test_reader_errors failed at line %d\n", rc); + return rc; + } + rc = test_arena(); + if (rc != 0) { + fprintf(stderr, "test_arena failed at line %d\n", rc); + return rc; + } + rc = test_json_value(); + if (rc != 0) { + fprintf(stderr, "test_json_value failed at line %d\n", rc); + return rc; + } + rc = test_json_value_borrowing(); + if (rc != 0) { + fprintf(stderr, "test_json_value_borrowing failed at line %d\n", rc); + return rc; + } + rc = test_depth_limits(); + if (rc != 0) { + fprintf(stderr, "test_depth_limits failed at line %d\n", rc); + return rc; + } + return 0; +}