From c614e0b8e02d2f9b8edd5f5ab7a0942310e3f845 Mon Sep 17 00:00:00 2001 From: GigabiteStudios Date: Sun, 14 Jun 2026 21:19:16 -0500 Subject: [PATCH] feat(core): split library layout and add tests --- .gitea/workflows/build.yml | 43 + README.md | 23 +- demo/build.bat | 39 + demo/main.c | 72 ++ demo/unit_test.c | 128 ++ include/ikv.h | 91 ++ src/ikv.c | 2243 +++++++++++++++++++++++++++++++++++ src/internal/ikv_internal.h | 66 ++ src/loaders/ikv1.c | 47 + src/loaders/ikv1.h | 5 + src/loaders/ikv2.c | 810 +++++++++++++ src/loaders/ikv2.h | 5 + 12 files changed, 3571 insertions(+), 1 deletion(-) create mode 100644 .gitea/workflows/build.yml create mode 100644 demo/build.bat create mode 100644 demo/main.c create mode 100644 demo/unit_test.c create mode 100644 include/ikv.h create mode 100644 src/ikv.c create mode 100644 src/internal/ikv_internal.h create mode 100644 src/loaders/ikv1.c create mode 100644 src/loaders/ikv1.h create mode 100644 src/loaders/ikv2.c create mode 100644 src/loaders/ikv2.h diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..08adf1d --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,43 @@ +name: Build + +on: + push: + pull_request: + +jobs: + linux-build-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install build tools + run: | + sudo apt-get update + sudo apt-get install -y build-essential + + - name: Build demo + run: | + mkdir -p demo/build + gcc -std=c11 -Wall -Wextra -pedantic \ + -Iinclude \ + demo/main.c \ + src/ikv.c \ + src/loaders/ikv1.c \ + src/loaders/ikv2.c \ + -o demo/build/ikv_demo + + - name: Build unit tests + run: | + mkdir -p demo/build + gcc -std=c11 -Wall -Wextra -pedantic \ + -Iinclude \ + demo/unit_test.c \ + src/ikv.c \ + src/loaders/ikv1.c \ + src/loaders/ikv2.c \ + -o demo/build/ikv_tests + + - name: Run unit tests + run: ./demo/build/ikv_tests diff --git a/README.md b/README.md index 2e942b5..18f13ca 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,24 @@ # iKv -Out internal iKv file format and C library \ No newline at end of file +Standalone iKv C library. + +Layout: +- `include/ikv.h`: public API only. +- `src/ikv.c`: shared implementation and loader dispatch. +- `src/loaders/ikv1.c`: iKv1 loader. +- `src/loaders/ikv2.c`: iKv2 loader. +- `src/internal/ikv_internal.h`: private node layout and loader interface. + +Behavior: +- The generic parse APIs auto-detect iKv1 vs iKv2 and choose the correct loader. +- The generic write APIs emit the current format version, which is `iKv2`. +- Public callers do not depend on loader-specific headers or internal structs. +- `iKv2` binary files now use an indexed root layout with a key table and payload offsets. +- Parsing an `iKv2` binary file reads the root index first and loads individual top-level values on demand. +- `iKv1` binary files still use the legacy full-tree format for compatibility. + +Adding a new version: +1. Add `src/loaders/ikv3.h` and `src/loaders/ikv3.c` exporting an `ikv_loader_t`. +2. Implement that loader using the shared internal helpers or custom logic. +3. Register the loader in `src/ikv.c`. +4. Add the new version constant to `include/ikv.h` if it is part of the public API. diff --git a/demo/build.bat b/demo/build.bat new file mode 100644 index 0000000..9409c40 --- /dev/null +++ b/demo/build.bat @@ -0,0 +1,39 @@ +@echo off +setlocal + +set ROOT_DIR=%~dp0.. +set OUT_DIR=%~dp0build +set DEMO_EXE=%OUT_DIR%\ikv_demo.exe +set TEST_EXE=%OUT_DIR%\ikv_tests.exe + +if not exist "%OUT_DIR%" mkdir "%OUT_DIR%" + +gcc -std=c11 -Wall -Wextra -pedantic ^ + -I"%ROOT_DIR%\include" ^ + "%~dp0main.c" ^ + "%ROOT_DIR%\src\ikv.c" ^ + "%ROOT_DIR%\src\loaders\ikv1.c" ^ + "%ROOT_DIR%\src\loaders\ikv2.c" ^ + -o "%DEMO_EXE%" + +if errorlevel 1 ( + echo demo build failed + exit /b 1 +) + +gcc -std=c11 -Wall -Wextra -pedantic ^ + -I"%ROOT_DIR%\include" ^ + "%~dp0unit_test.c" ^ + "%ROOT_DIR%\src\ikv.c" ^ + "%ROOT_DIR%\src\loaders\ikv1.c" ^ + "%ROOT_DIR%\src\loaders\ikv2.c" ^ + -o "%TEST_EXE%" + +if errorlevel 1 ( + echo unit test build failed + exit /b 1 +) + +echo built "%DEMO_EXE%" +echo built "%TEST_EXE%" +endlocal diff --git a/demo/main.c b/demo/main.c new file mode 100644 index 0000000..f834ac3 --- /dev/null +++ b/demo/main.c @@ -0,0 +1,72 @@ +#include "ikv.h" + +#include +#include + +int main(void) +{ + ikv_node_t *root = ikv_create_object("demo"); + ikv_node_t *player = NULL; + ikv_node_t *inventory = NULL; + ikv_node_t *loaded = NULL; + uint8_t *binary = NULL; + uint32_t binary_size = 0u; + + if (!root) + { + fputs("failed to create root\n", stderr); + return 1; + } + + ikv_object_set_string(root, "title", "iKv demo"); + ikv_object_set_int(root, "version", 2); + + player = ikv_object_add_object(root, "player"); + inventory = ikv_object_add_array(root, "inventory", IKV_STRING); + if (!player || !inventory) + { + ikv_free(root); + fputs("failed to build demo tree\n", stderr); + return 1; + } + + ikv_object_set_string(player, "name", "jondoe"); + ikv_object_set_bool(player, "alive", true); + ikv_object_set_float(player, "speed", 12.5); + + ikv_array_add_string(inventory, "wrench"); + ikv_array_add_string(inventory, "battery"); + ikv_array_add_string(inventory, "map"); + + if (!ikv_write_file("demo_output.ikv", root)) + { + ikv_free(root); + fputs("failed to write demo_output.ikv\n", stderr); + return 1; + } + + if (!ikvb_write_memory(root, &binary, &binary_size)) + { + ikv_free(root); + fputs("failed to write binary output\n", stderr); + return 1; + } + + loaded = ikvb_parse_memory(binary, binary_size); + if (!loaded) + { + free(binary); + ikv_free(root); + fputs("failed to parse binary output\n", stderr); + return 1; + } + + printf("title: %s\n", ikv_as_string(ikv_object_get(loaded, "title"))); + printf("version: %lld\n", (long long)ikv_as_int(ikv_object_get(loaded, "version"))); + printf("inventory items: %u\n", ikv_array_size(ikv_object_get(loaded, "inventory"))); + + ikv_free(loaded); + free(binary); + ikv_free(root); + return 0; +} diff --git a/demo/unit_test.c b/demo/unit_test.c new file mode 100644 index 0000000..6fbd562 --- /dev/null +++ b/demo/unit_test.c @@ -0,0 +1,128 @@ +#include "ikv.h" + +#include +#include +#include + +static int fail(const char *message) +{ + fputs(message, stderr); + fputc('\n', stderr); + return 1; +} + +static int test_text_roundtrip(void) +{ + const char *src = + "ikv1 \"root\"\n" + "{\n" + " \"name\" \"legacy\"\n" + " \"count\" 7\n" + "}\n"; + ikv_node_t *root = ikv_parse_string(src); + + if (!root) + return fail("text parse failed"); + if (ikv_node_type(root) != IKV_OBJECT) + return fail("text root type mismatch"); + if (strcmp(ikv_as_string(ikv_object_get(root, "name")), "legacy") != 0) + return fail("text string value mismatch"); + if (ikv_as_int(ikv_object_get(root, "count")) != 7) + return fail("text int value mismatch"); + + ikv_free(root); + return 0; +} + +static int test_binary_v1_roundtrip(void) +{ + ikv_node_t *root = ikv_create_object("root"); + ikv_node_t *loaded = NULL; + uint8_t *data = NULL; + uint32_t size = 0u; + int status = 0; + + if (!root) + return fail("v1 root allocation failed"); + + ikv_object_set_int(root, "value", 11); + if (!ikvb_write_memory_version(root, &data, &size, IKV_VERSION_1)) + status = fail("v1 binary write failed"); + else + { + loaded = ikvb_parse_memory(data, size); + if (!loaded) + status = fail("v1 binary parse failed"); + else if (ikv_as_int(ikv_object_get(loaded, "value")) != 11) + status = fail("v1 binary value mismatch"); + } + + ikv_free(loaded); + free(data); + ikv_free(root); + return status; +} + +static int test_binary_v2_lazy_root(void) +{ + ikv_node_t *root = ikv_create_object("root"); + ikv_node_t *nested = NULL; + ikv_node_t *loaded = NULL; + ikv_node_t *loaded_nested = NULL; + uint8_t *data = NULL; + uint32_t size = 0u; + int status = 0; + + if (!root) + return fail("v2 root allocation failed"); + + ikv_object_set_string(root, "title", "fast"); + nested = ikv_object_add_object(root, "nested"); + if (!nested) + { + ikv_free(root); + return fail("v2 nested allocation failed"); + } + + ikv_object_set_bool(nested, "flag", true); + ikv_object_set_float(nested, "speed", 9.25); + + if (!ikvb_write_memory_version(root, &data, &size, IKV_VERSION_2)) + status = fail("v2 binary write failed"); + else + { + loaded = ikvb_parse_memory(data, size); + if (!loaded) + status = fail("v2 binary parse failed"); + else if (ikv_object_size(loaded) != 2u) + status = fail("v2 root key count mismatch"); + else if (strcmp(ikv_as_string(ikv_object_get(loaded, "title")), "fast") != 0) + status = fail("v2 root lazy lookup failed"); + else + { + loaded_nested = ikv_object_get(loaded, "nested"); + if (!loaded_nested) + status = fail("v2 nested lazy lookup failed"); + else if (!ikv_as_bool(ikv_object_get(loaded_nested, "flag"))) + status = fail("v2 nested bool mismatch"); + } + } + + ikv_free(loaded); + free(data); + ikv_free(root); + return status; +} + +int main(void) +{ + if (test_text_roundtrip() != 0) + return 1; + if (test_binary_v1_roundtrip() != 0) + return 1; + if (test_binary_v2_lazy_root() != 0) + return 1; + + puts("all tests passed"); + return 0; +} diff --git a/include/ikv.h b/include/ikv.h new file mode 100644 index 0000000..9925ef7 --- /dev/null +++ b/include/ikv.h @@ -0,0 +1,91 @@ +#pragma once + +#include +#include +#include + +#define IKV_V1 1u +#define IKV_V2 2u +#define IKV_CURRENT_VERSION IKV_V2 + +typedef struct ikv_node_t ikv_node_t; + +typedef enum +{ + IKV_VERSION_UNKNOWN = 0, + IKV_VERSION_1 = IKV_V1, + IKV_VERSION_2 = IKV_V2 +} ikv_version_t; + +typedef enum +{ + IKV_NULL = 0, + IKV_STRING, + IKV_INT, + IKV_FLOAT, + IKV_BOOL, + IKV_OBJECT, + IKV_ARRAY +} ikv_type_t; + +/* Node lifetime */ +ikv_node_t *ikv_create_object(const char *key); +ikv_node_t *ikv_create_array(const char *key, ikv_type_t element_type); +void ikv_free(ikv_node_t *node); + +/* Node metadata */ +const char *ikv_node_key(const ikv_node_t *node); +ikv_type_t ikv_node_type(const ikv_node_t *node); + +/* Object access */ +ikv_node_t *ikv_object_get(const ikv_node_t *object_node, const char *key); +uint32_t ikv_object_size(const ikv_node_t *object_node); + +/* Object mutation */ +ikv_node_t *ikv_object_add_object(ikv_node_t *object_node, const char *key); +ikv_node_t *ikv_object_add_array(ikv_node_t *object_node, const char *key, ikv_type_t element_type); +void ikv_object_set_int(ikv_node_t *object_node, const char *key, int64_t value); +void ikv_object_set_float(ikv_node_t *object_node, const char *key, double value); +void ikv_object_set_bool(ikv_node_t *object_node, const char *key, bool value); +void ikv_object_set_string(ikv_node_t *object_node, const char *key, const char *value); + +/* Array access */ +ikv_node_t *ikv_array_get(const ikv_node_t *array_node, uint32_t index); +uint32_t ikv_array_size(const ikv_node_t *array_node); +ikv_type_t ikv_array_element_type(const ikv_node_t *array_node); + +/* Array mutation */ +ikv_node_t *ikv_array_add_object(ikv_node_t *array_node); +void ikv_array_add_int(ikv_node_t *array_node, int64_t value); +void ikv_array_add_float(ikv_node_t *array_node, double value); +void ikv_array_add_bool(ikv_node_t *array_node, bool value); +void ikv_array_add_string(ikv_node_t *array_node, const char *value); + +/* Scalar reads */ +const char *ikv_as_string(const ikv_node_t *node); +int64_t ikv_as_int(const ikv_node_t *node); +double ikv_as_float(const ikv_node_t *node); +bool ikv_as_bool(const ikv_node_t *node); + +/* Version detection */ +ikv_version_t ikv_detect_text_version(const char *src); +ikv_version_t ikv_detect_binary_version(const void *data, size_t size); +ikv_version_t ikv_detect_file_version(const char *path, bool binary); + +/* Text I/O */ +ikv_node_t *ikv_parse_string(const char *src); +ikv_node_t *ikv_parse_string_version(const char *src, ikv_version_t version); +ikv_node_t *ikv_parse_file(const char *path); +ikv_node_t *ikv_parse_file_version(const char *path, ikv_version_t version); +bool ikv_write_file(const char *path, const ikv_node_t *root); +bool ikv_write_file_version(const char *path, const ikv_node_t *root, ikv_version_t version); + +/* Binary I/O */ +ikv_node_t *ikvb_parse_memory(const void *data, size_t size); +ikv_node_t *ikvb_parse_memory_version(const void *data, size_t size, ikv_version_t version); +ikv_node_t *ikvb_parse_file(const char *path); +ikv_node_t *ikvb_parse_file_version(const char *path, ikv_version_t version); +bool ikvb_write_memory(const ikv_node_t *root, uint8_t **out_data, uint32_t *out_size); +bool ikvb_write_memory_version(const ikv_node_t *root, uint8_t **out_data, uint32_t *out_size, ikv_version_t version); +bool ikvb_write_file(const char *path, const ikv_node_t *root); +bool ikvb_write_file_version(const char *path, const ikv_node_t *root, ikv_version_t version); diff --git a/src/ikv.c b/src/ikv.c new file mode 100644 index 0000000..3a8b344 --- /dev/null +++ b/src/ikv.c @@ -0,0 +1,2243 @@ +#include "internal/ikv_internal.h" +#include "loaders/ikv1.h" +#include "loaders/ikv2.h" +#include +#include +#include +#include + +#define IKV_MALLOC malloc +#define IKV_CALLOC calloc +#define IKV_REALLOC realloc +#define IKV_FREE free + +static const ikv_loader_t *const ikv_loader_registry[] = { + &ikv_loader_v1, + &ikv_loader_v2, +}; + +static size_t ikv_loader_registry_count(void) +{ + return sizeof(ikv_loader_registry) / sizeof(ikv_loader_registry[0]); +} + +static const ikv_loader_t *ikv_find_loader(ikv_version_t version) +{ + size_t index = 0; + + for (index = 0; index < ikv_loader_registry_count(); ++index) + { + if (ikv_loader_registry[index]->version == version) + return ikv_loader_registry[index]; + } + + return NULL; +} + +static bool read_file_bytes(const char *path, uint8_t **out_data, size_t *out_size) +{ + FILE *f = NULL; + long size_long = 0; + size_t size = 0; + uint8_t *data = NULL; + + if (!path || !out_data || !out_size) + return false; + + *out_data = NULL; + *out_size = 0; + + f = fopen(path, "rb"); + if (!f) + return false; + + if (fseek(f, 0, SEEK_END) != 0) + { + fclose(f); + return false; + } + + size_long = ftell(f); + if (size_long < 0) + { + fclose(f); + return false; + } + + if (fseek(f, 0, SEEK_SET) != 0) + { + fclose(f); + return false; + } + + size = (size_t)size_long; + data = (uint8_t *)IKV_MALLOC(size + 1u); + if (!data) + { + fclose(f); + return false; + } + + if (size > 0 && fread(data, 1, size, f) != size) + { + IKV_FREE(data); + fclose(f); + return false; + } + + fclose(f); + data[size] = 0; + *out_data = data; + *out_size = size; + return true; +} + +static char *ikv_strdup(const char *s) +{ + if (!s) + return NULL; + size_t n = strlen(s); + char *p = (char *)IKV_MALLOC(n + 1); + if (!p) + return NULL; + memcpy(p, s, n + 1); + return p; +} + +static uint32_t fnv1a(const char *s) +{ + uint32_t h = 2166136261u; + while (s && *s) + { + h ^= (uint8_t)*s++; + h *= 16777619u; + } + return h; +} + +static ikv_node_t *alloc_node(const char *key) +{ + ikv_node_t *n = (ikv_node_t *)IKV_CALLOC(1, sizeof(*n)); + if (!n) + return NULL; + if (key) + { + n->key = ikv_strdup(key); + if (!n->key) + { + IKV_FREE(n); + return NULL; + } + } + return n; +} + +static void object_init(ikv_object_t *o, uint32_t buckets) +{ + o->bucket_count = buckets ? buckets : 64; + o->size = 0; + o->buckets = (ikv_node_t **)IKV_CALLOC(o->bucket_count, sizeof(ikv_node_t *)); +} + +static void array_init(ikv_array_t *a, ikv_type_t element_type) +{ + a->element_type = element_type; + a->count = 0; + a->items = NULL; +} + +static void node_free_payload(ikv_node_t *n) +{ + if (!n) + return; + if (n->type == IKV_STRING) + { + IKV_FREE(n->value.string); + } + else if (n->type == IKV_OBJECT) + { + for (uint32_t i = 0; i < n->value.object.bucket_count; i++) + { + ikv_node_t *c = n->value.object.buckets[i]; + while (c) + { + ikv_node_t *next = c->next; + ikv_free(c); + c = next; + } + } + IKV_FREE(n->value.object.buckets); + } + else if (n->type == IKV_ARRAY) + { + for (uint32_t i = 0; i < n->value.array.count; i++) + ikv_free(n->value.array.items[i]); + IKV_FREE(n->value.array.items); + } + + if (n->lazy_state) + { + n->lazy_state->destroy(n->lazy_state); + n->lazy_state = NULL; + } +} + +void ikv_free(ikv_node_t *n) +{ + if (!n) + return; + IKV_FREE(n->key); + node_free_payload(n); + IKV_FREE(n); +} + +const char *ikv_node_key(const ikv_node_t *node) +{ + return (node && node->key) ? node->key : ""; +} + +ikv_type_t ikv_node_type(const ikv_node_t *node) +{ + return node ? node->type : IKV_NULL; +} + +uint32_t ikv_object_size(const ikv_node_t *object_node) +{ + if (!object_node || object_node->type != IKV_OBJECT) + return 0u; + return object_node->value.object.size; +} + +uint32_t ikv_array_size(const ikv_node_t *array_node) +{ + if (!array_node || array_node->type != IKV_ARRAY) + return 0u; + return array_node->value.array.count; +} + +ikv_type_t ikv_array_element_type(const ikv_node_t *array_node) +{ + if (!array_node || array_node->type != IKV_ARRAY) + return IKV_NULL; + return array_node->value.array.element_type; +} + +ikv_node_t *ikv_create_object(const char *key) +{ + ikv_node_t *n = alloc_node(key); + if (!n) + return NULL; + n->type = IKV_OBJECT; + object_init(&n->value.object, 64); + if (!n->value.object.buckets) + { + ikv_free(n); + return NULL; + } + return n; +} + +ikv_node_t *ikv_create_array(const char *key, ikv_type_t element_type) +{ + ikv_node_t *n = alloc_node(key); + if (!n) + return NULL; + n->type = IKV_ARRAY; + array_init(&n->value.array, element_type); + return n; +} + +static ikv_node_t *object_find_node(const ikv_object_t *o, const char *key, ikv_node_t **out_prev, uint32_t *out_bucket) +{ + if (out_prev) + *out_prev = NULL; + if (out_bucket) + *out_bucket = 0; + if (!o || !o->buckets || !key) + return NULL; + + uint32_t b = fnv1a(key) % o->bucket_count; + if (out_bucket) + *out_bucket = b; + + ikv_node_t *prev = NULL; + ikv_node_t *cur = o->buckets[b]; + while (cur) + { + if (cur->key && strcmp(cur->key, key) == 0) + { + if (out_prev) + *out_prev = prev; + return cur; + } + prev = cur; + cur = cur->next; + } + if (out_prev) + *out_prev = prev; + return NULL; +} + +static void object_rehash(ikv_object_t *o, uint32_t new_bucket_count) +{ + ikv_node_t **new_buckets = (ikv_node_t **)IKV_CALLOC(new_bucket_count, sizeof(ikv_node_t *)); + if (!new_buckets) + return; + + for (uint32_t i = 0; i < o->bucket_count; i++) + { + ikv_node_t *c = o->buckets[i]; + while (c) + { + ikv_node_t *next = c->next; + uint32_t b = fnv1a(c->key) % new_bucket_count; + c->next = new_buckets[b]; + new_buckets[b] = c; + c = next; + } + } + + IKV_FREE(o->buckets); + o->buckets = new_buckets; + o->bucket_count = new_bucket_count; +} + +static void object_maybe_grow(ikv_object_t *o) +{ + if (!o || o->bucket_count == 0) + return; + if (o->size * 4 >= o->bucket_count * 3) + object_rehash(o, o->bucket_count * 2); +} + +static void object_put_node(ikv_object_t *o, ikv_node_t *n) +{ + if (!o || !n || !n->key) + return; + + object_maybe_grow(o); + + uint32_t b = fnv1a(n->key) % o->bucket_count; + n->next = o->buckets[b]; + o->buckets[b] = n; + o->size++; +} + +static void object_attach_loaded_node(ikv_object_t *o, ikv_node_t *n) +{ + uint32_t b = 0; + + if (!o || !n || !n->key) + return; + + object_maybe_grow(o); + b = fnv1a(n->key) % o->bucket_count; + n->next = o->buckets[b]; + o->buckets[b] = n; +} + +static void object_set_node(ikv_object_t *o, ikv_node_t *n) +{ + if (!o || !n || !n->key) + { + ikv_free(n); + return; + } + + uint32_t bucket = 0; + ikv_node_t *prev = NULL; + ikv_node_t *found = object_find_node(o, n->key, &prev, &bucket); + + if (found) + { + if (prev) + prev->next = found->next; + else + o->buckets[bucket] = found->next; + o->size--; + ikv_free(found); + } + + object_put_node(o, n); +} + +void ikv_object_set_int(ikv_node_t *obj, const char *key, int64_t v) +{ + if (!obj || obj->type != IKV_OBJECT || !key) + return; + ikv_node_t *n = alloc_node(key); + if (!n) + return; + n->type = IKV_INT; + n->value.i = v; + object_set_node(&obj->value.object, n); +} + +void ikv_object_set_bool(ikv_node_t *obj, const char *key, bool v) +{ + if (!obj || obj->type != IKV_OBJECT || !key) + return; + ikv_node_t *n = alloc_node(key); + if (!n) + return; + n->type = IKV_BOOL; + n->value.b = v; + object_set_node(&obj->value.object, n); +} + +void ikv_object_set_float(ikv_node_t *obj, const char *key, double v) +{ + if (!obj || obj->type != IKV_OBJECT || !key) + return; + ikv_node_t *n = alloc_node(key); + if (!n) + return; + n->type = IKV_FLOAT; + n->value.f = v; + object_set_node(&obj->value.object, n); +} + +void ikv_object_set_string(ikv_node_t *obj, const char *key, const char *v) +{ + if (!obj || obj->type != IKV_OBJECT || !key) + return; + ikv_node_t *n = alloc_node(key); + if (!n) + return; + n->type = IKV_STRING; + n->value.string = ikv_strdup(v ? v : ""); + if (!n->value.string) + { + ikv_free(n); + return; + } + object_set_node(&obj->value.object, n); +} + +ikv_node_t *ikv_object_add_object(ikv_node_t *obj, const char *key) +{ + if (!obj || obj->type != IKV_OBJECT || !key) + return NULL; + ikv_node_t *n = ikv_create_object(key); + if (!n) + return NULL; + object_set_node(&obj->value.object, n); + return n; +} + +ikv_node_t *ikv_object_add_array(ikv_node_t *obj, const char *key, ikv_type_t t) +{ + if (!obj || obj->type != IKV_OBJECT || !key) + return NULL; + ikv_node_t *n = ikv_create_array(key, t); + if (!n) + return NULL; + object_set_node(&obj->value.object, n); + return n; +} + +static void array_push_node(ikv_array_t *a, ikv_node_t *n) +{ + ikv_node_t **p = (ikv_node_t **)IKV_REALLOC(a->items, sizeof(ikv_node_t *) * (a->count + 1)); + if (!p) + { + ikv_free(n); + return; + } + a->items = p; + a->items[a->count++] = n; +} + +static bool array_type_ok(ikv_array_t *a, ikv_type_t t) +{ + if (!a) + return false; + if (a->element_type == IKV_NULL) + return true; + return a->element_type == t; +} + +void ikv_array_add_int(ikv_node_t *arr, int64_t v) +{ + if (!arr || arr->type != IKV_ARRAY) + return; + if (!array_type_ok(&arr->value.array, IKV_INT)) + return; + ikv_node_t *n = alloc_node(NULL); + if (!n) + return; + n->type = IKV_INT; + n->value.i = v; + array_push_node(&arr->value.array, n); +} + +void ikv_array_add_bool(ikv_node_t *arr, bool v) +{ + if (!arr || arr->type != IKV_ARRAY) + return; + if (!array_type_ok(&arr->value.array, IKV_BOOL)) + return; + ikv_node_t *n = alloc_node(NULL); + if (!n) + return; + n->type = IKV_BOOL; + n->value.b = v; + array_push_node(&arr->value.array, n); +} + +void ikv_array_add_float(ikv_node_t *arr, double v) +{ + if (!arr || arr->type != IKV_ARRAY) + return; + if (!array_type_ok(&arr->value.array, IKV_FLOAT)) + return; + ikv_node_t *n = alloc_node(NULL); + if (!n) + return; + n->type = IKV_FLOAT; + n->value.f = v; + array_push_node(&arr->value.array, n); +} + +void ikv_array_add_string(ikv_node_t *arr, const char *v) +{ + if (!arr || arr->type != IKV_ARRAY) + return; + if (!array_type_ok(&arr->value.array, IKV_STRING)) + return; + ikv_node_t *n = alloc_node(NULL); + if (!n) + return; + n->type = IKV_STRING; + n->value.string = ikv_strdup(v ? v : ""); + if (!n->value.string) + { + ikv_free(n); + return; + } + array_push_node(&arr->value.array, n); +} + +ikv_node_t *ikv_array_add_object(ikv_node_t *arr) +{ + if (!arr || arr->type != IKV_ARRAY) + return NULL; + if (!array_type_ok(&arr->value.array, IKV_OBJECT)) + return NULL; + ikv_node_t *n = ikv_create_object(NULL); + if (!n) + return NULL; + array_push_node(&arr->value.array, n); + return n; +} + +ikv_node_t *ikv_object_get(const ikv_node_t *obj, const char *key) +{ + ikv_node_t *n = NULL; + ikv_node_t *loaded = NULL; + ikv_node_t *mutable_obj = (ikv_node_t *)obj; + + if (!obj || obj->type != IKV_OBJECT || !key) + return NULL; + + if (obj->value.object.bucket_count == 0) + return NULL; + + { + uint32_t b = fnv1a(key) % obj->value.object.bucket_count; + n = obj->value.object.buckets[b]; + } + + while (n) + { + if (n->key && strcmp(n->key, key) == 0) + return n; + n = n->next; + } + + if (!mutable_obj->lazy_state || !mutable_obj->lazy_state->load_object_key) + return NULL; + + loaded = mutable_obj->lazy_state->load_object_key(mutable_obj->lazy_state, mutable_obj, key); + if (!loaded) + return NULL; + + if (!loaded->key) + { + loaded->key = ikv_strdup(key); + if (!loaded->key) + { + ikv_free(loaded); + return NULL; + } + } + + object_attach_loaded_node(&mutable_obj->value.object, loaded); + return loaded; +} + +ikv_node_t *ikv_array_get(const ikv_node_t *arr, uint32_t index) +{ + if (!arr || arr->type != IKV_ARRAY) + return NULL; + if (index >= arr->value.array.count) + return NULL; + return arr->value.array.items[index]; +} + +const char *ikv_as_string(const ikv_node_t *n) { return (n && n->type == IKV_STRING && n->value.string) ? n->value.string : ""; } +int64_t ikv_as_int(const ikv_node_t *n) { return (n && n->type == IKV_INT) ? n->value.i : 0; } +double ikv_as_float(const ikv_node_t *n) { return (n && n->type == IKV_FLOAT) ? n->value.f : 0.0; } +bool ikv_as_bool(const ikv_node_t *n) { return (n && n->type == IKV_BOOL) ? n->value.b : false; } + +static void write_indent(FILE *f, int indent) +{ + for (int i = 0; i < indent; i++) + fputc(' ', f); +} + +static void write_escaped_string(FILE *f, const char *s) +{ + fputc('"', f); + for (const char *p = s ? s : ""; *p; p++) + { + unsigned char c = (unsigned char)*p; + if (c == '\\') + { + fputs("\\\\", f); + } + else if (c == '"') + { + fputs("\\\"", f); + } + else if (c == '\n') + { + fputs("\\n", f); + } + else if (c == '\r') + { + fputs("\\r", f); + } + else if (c == '\t') + { + fputs("\\t", f); + } + else + { + fputc(c, f); + } + } + fputc('"', f); +} + +static void write_node(FILE *f, const ikv_node_t *n, int indent); + +static void write_value(FILE *f, const ikv_node_t *n, int indent) +{ + switch (n->type) + { + case IKV_NULL: + fputs("null", f); + break; + case IKV_STRING: + write_escaped_string(f, n->value.string ? n->value.string : ""); + break; + case IKV_INT: + fprintf(f, "%lld", (long long)n->value.i); + break; + case IKV_BOOL: + fputs(n->value.b ? "true" : "false", f); + break; + case IKV_FLOAT: + { + char buf[64]; + snprintf(buf, sizeof(buf), "%.17g", n->value.f); + fputs(buf, f); + } + break; + case IKV_OBJECT: + { + fputs("{\n", f); + for (uint32_t i = 0; i < n->value.object.bucket_count; i++) + { + for (ikv_node_t *c = n->value.object.buckets[i]; c; c = c->next) + write_node(f, c, indent + 4); + } + write_indent(f, indent); + fputc('}', f); + } + break; + case IKV_ARRAY: + { + fputs("[\n", f); + for (uint32_t i = 0; i < n->value.array.count; i++) + { + write_indent(f, indent + 4); + write_value(f, n->value.array.items[i], indent + 4); + fputc('\n', f); + } + write_indent(f, indent); + fputc(']', f); + } + break; + default: + break; + } +} + +static void write_node(FILE *f, const ikv_node_t *n, int indent) +{ + write_indent(f, indent); + if (n->key) + { + write_escaped_string(f, n->key); + fputc(' ', f); + } + write_value(f, n, indent); + fputc('\n', f); +} + +bool ikv__write_text_file_version(const char *path, const ikv_node_t *root, uint32_t version) +{ + if (!path || !root) + return false; + + FILE *f = fopen(path, "wb"); + if (!f) + return false; + + if (root->type != IKV_OBJECT) + { + write_value(f, root, 0); + fputc('\n', f); + fclose(f); + return true; + } + + fprintf(f, "ikv%u ", version); + write_escaped_string(f, (root->key && root->key[0]) ? root->key : "root"); + fputc('\n', f); + + fputs("{\n", f); + for (uint32_t i = 0; i < root->value.object.bucket_count; i++) + { + for (ikv_node_t *c = root->value.object.buckets[i]; c; c = c->next) + write_node(f, c, 4); + } + fputs("}\n", f); + + fclose(f); + return true; +} + +typedef enum +{ + T_EOF, + T_LBRACE, + T_RBRACE, + T_LBRACK, + T_RBRACK, + T_COMMA, + T_STRING, + T_WORD +} tok_type; + +typedef struct +{ + tok_type type; + const char *start; + size_t len; +} token; + +typedef struct +{ + const char *p; +} lexer; + +static const char *lex_skip_ws(const char *p) +{ + while (*p) + { + if (isspace((unsigned char)*p)) + { + p++; + continue; + } + if (p[0] == '/' && p[1] == '/') + { + p += 2; + while (*p && *p != '\n') + p++; + continue; + } + if (p[0] == '#') + { + p += 1; + while (*p && *p != '\n') + p++; + continue; + } + break; + } + return p; +} + +static token lex_next(lexer *l) +{ + token t; + t.type = T_EOF; + t.start = l->p; + t.len = 0; + + l->p = lex_skip_ws(l->p); + const char *p = l->p; + if (!*p) + { + t.type = T_EOF; + return t; + } + + if (*p == '{') + { + l->p = p + 1; + t.type = T_LBRACE; + return t; + } + if (*p == '}') + { + l->p = p + 1; + t.type = T_RBRACE; + return t; + } + if (*p == '[') + { + l->p = p + 1; + t.type = T_LBRACK; + return t; + } + if (*p == ']') + { + l->p = p + 1; + t.type = T_RBRACK; + return t; + } + if (*p == ',') + { + l->p = p + 1; + t.type = T_COMMA; + return t; + } + + if (*p == '"') + { + p++; + const char *s = p; + while (*p) + { + if (*p == '\\' && p[1]) + { + p += 2; + continue; + } + if (*p == '"') + break; + p++; + } + if (*p != '"') + { + l->p = p; + t.type = T_EOF; + return t; + } + t.type = T_STRING; + t.start = s; + t.len = (size_t)(p - s); + l->p = p + 1; + return t; + } + + const char *s = p; + while (*p && !isspace((unsigned char)*p) && *p != '{' && *p != '}' && *p != '[' && *p != ']' && *p != ',') + p++; + t.type = T_WORD; + t.start = s; + t.len = (size_t)(p - s); + l->p = p; + return t; +} + +static char *unescape_string(const char *s, size_t len) +{ + char *out = (char *)IKV_MALLOC(len + 1); + if (!out) + return NULL; + size_t w = 0; + for (size_t i = 0; i < len; i++) + { + char c = s[i]; + if (c == '\\' && i + 1 < len) + { + char n = s[i + 1]; + if (n == 'n') + { + out[w++] = '\n'; + i++; + continue; + } + if (n == 'r') + { + out[w++] = '\r'; + i++; + continue; + } + if (n == 't') + { + out[w++] = '\t'; + i++; + continue; + } + if (n == '\\') + { + out[w++] = '\\'; + i++; + continue; + } + if (n == '"') + { + out[w++] = '"'; + i++; + continue; + } + } + out[w++] = c; + } + out[w] = 0; + return out; +} + +static char *token_to_cstr(const token *t) +{ + char *s = (char *)IKV_MALLOC(t->len + 1); + if (!s) + return NULL; + memcpy(s, t->start, t->len); + s[t->len] = 0; + return s; +} + +static ikv_node_t *parse_value_lex(lexer *l); + +static ikv_node_t *parse_array_lex(lexer *l) +{ + ikv_node_t *arr = ikv_create_array(NULL, IKV_NULL); + if (!arr) + return NULL; + + for (;;) + { + const char *save = l->p; + token t = lex_next(l); + if (t.type == T_RBRACK) + break; + if (t.type == T_EOF) + { + ikv_free(arr); + return NULL; + } + if (t.type == T_COMMA) + continue; + l->p = save; + + ikv_node_t *v = parse_value_lex(l); + if (!v) + { + ikv_free(arr); + return NULL; + } + + if (arr->value.array.element_type == IKV_NULL && v->type != IKV_NULL) + arr->value.array.element_type = v->type; + + if (arr->value.array.element_type != IKV_NULL && v->type != IKV_NULL && v->type != arr->value.array.element_type) + arr->value.array.element_type = IKV_NULL; + + array_push_node(&arr->value.array, v); + + save = l->p; + t = lex_next(l); + if (t.type == T_RBRACK) + break; + if (t.type == T_EOF) + { + ikv_free(arr); + return NULL; + } + if (t.type != T_COMMA) + l->p = save; + } + + return arr; +} + +static ikv_node_t *parse_object_lex(lexer *l) +{ + ikv_node_t *obj = ikv_create_object(NULL); + if (!obj) + return NULL; + + for (;;) + { + token k = lex_next(l); + if (k.type == T_RBRACE) + break; + if (k.type == T_EOF) + { + ikv_free(obj); + return NULL; + } + if (k.type == T_COMMA) + continue; + if (k.type != T_STRING) + { + ikv_free(obj); + return NULL; + } + + char *key = unescape_string(k.start, k.len); + if (!key) + { + ikv_free(obj); + return NULL; + } + + ikv_node_t *val = parse_value_lex(l); + if (!val) + { + IKV_FREE(key); + ikv_free(obj); + return NULL; + } + + IKV_FREE(val->key); + val->key = key; + object_set_node(&obj->value.object, val); + + const char *save = l->p; + token t = lex_next(l); + if (t.type == T_RBRACE) + break; + if (t.type == T_EOF) + { + ikv_free(obj); + return NULL; + } + if (t.type != T_COMMA) + l->p = save; + } + + return obj; +} + +static ikv_node_t *parse_value_lex(lexer *l) +{ + token t = lex_next(l); + + if (t.type == T_LBRACE) + return parse_object_lex(l); + if (t.type == T_LBRACK) + return parse_array_lex(l); + + if (t.type == T_STRING) + { + ikv_node_t *n = alloc_node(NULL); + if (!n) + return NULL; + n->type = IKV_STRING; + n->value.string = unescape_string(t.start, t.len); + if (!n->value.string) + { + ikv_free(n); + return NULL; + } + return n; + } + + if (t.type == T_WORD) + { + char *w = token_to_cstr(&t); + if (!w) + return NULL; + + ikv_node_t *n = alloc_node(NULL); + if (!n) + { + IKV_FREE(w); + return NULL; + } + + if (strcmp(w, "true") == 0) + { + n->type = IKV_BOOL; + n->value.b = true; + IKV_FREE(w); + return n; + } + if (strcmp(w, "false") == 0) + { + n->type = IKV_BOOL; + n->value.b = false; + IKV_FREE(w); + return n; + } + if (strcmp(w, "null") == 0) + { + n->type = IKV_NULL; + IKV_FREE(w); + return n; + } + + char *endp = NULL; + double df = strtod(w, &endp); + if (endp && *endp == 0) + { + if (strchr(w, '.') || strchr(w, 'e') || strchr(w, 'E')) + { + n->type = IKV_FLOAT; + n->value.f = df; + IKV_FREE(w); + return n; + } + else + { + long long di = strtoll(w, &endp, 10); + if (endp && *endp == 0) + { + n->type = IKV_INT; + n->value.i = (int64_t)di; + IKV_FREE(w); + return n; + } + } + } + + n->type = IKV_STRING; + n->value.string = w; + return n; + } + + return NULL; +} + +ikv_node_t *ikv__parse_text_string_version(const char *src, uint32_t version) +{ + if (!src) + return NULL; + + lexer l; + l.p = src; + + const char *save0 = l.p; + token t0 = lex_next(&l); + + if (t0.type == T_WORD) + { + char *w0 = token_to_cstr(&t0); + if (!w0) + return NULL; + + char version_tag[16]; + snprintf(version_tag, sizeof(version_tag), "ikv%u", version); + + if (strcmp(w0, version_tag) == 0) + { + IKV_FREE(w0); + + token tn = lex_next(&l); + if (tn.type != T_STRING && tn.type != T_WORD) + return NULL; + + char *name = (tn.type == T_STRING) ? unescape_string(tn.start, tn.len) : token_to_cstr(&tn); + if (!name) + return NULL; + + token tb = lex_next(&l); + if (tb.type != T_LBRACE) + { + IKV_FREE(name); + return NULL; + } + + ikv_node_t *obj = parse_object_lex(&l); + if (!obj) + { + IKV_FREE(name); + return NULL; + } + + IKV_FREE(obj->key); + obj->key = name; + return obj; + } + + IKV_FREE(w0); + l.p = save0; + } + else if (t0.type == T_LBRACE) + { + return parse_object_lex(&l); + } + else + { + l.p = save0; + } + + ikv_node_t *root = ikv_create_object(NULL); + if (!root) + return NULL; + + for (;;) + { + const char *s2 = l.p; + token k = lex_next(&l); + if (k.type == T_EOF) + break; + if (k.type == T_COMMA) + continue; + if (k.type != T_STRING) + { + ikv_free(root); + return NULL; + } + + char *key = unescape_string(k.start, k.len); + if (!key) + { + ikv_free(root); + return NULL; + } + + ikv_node_t *val = parse_value_lex(&l); + if (!val) + { + IKV_FREE(key); + ikv_free(root); + return NULL; + } + + IKV_FREE(val->key); + val->key = key; + object_set_node(&root->value.object, val); + + s2 = l.p; + token sep = lex_next(&l); + if (sep.type == T_EOF) + break; + if (sep.type != T_COMMA) + l.p = s2; + } + + return root; +} + +ikv_node_t *ikv__parse_text_file_version(const char *path, uint32_t version) +{ + if (!path) + return NULL; + + uint8_t *bytes = NULL; + size_t bytes_n = 0u; + char *buf = NULL; + ikv_node_t *root = NULL; + + if (!read_file_bytes(path, &bytes, &bytes_n) || !bytes) + return NULL; + + buf = (char *)IKV_MALLOC(bytes_n + 1u); + if (!buf) + { + IKV_FREE(bytes); + return NULL; + } + if (bytes_n > 0u) + memcpy(buf, bytes, bytes_n); + buf[bytes_n] = 0; + IKV_FREE(bytes); + + root = ikv__parse_text_string_version(buf, version); + IKV_FREE(buf); + return root; +} + +typedef struct +{ + void *ctx; + bool ok; + void (*write_bytes_fn)(void *ctx, bool *ok, const void *p, size_t n); +} bw_t; + +typedef struct +{ + uint8_t *data; + size_t size; + size_t cap; +} bw_mem_t; + +static bool bw_mem_reserve(bw_mem_t *m, size_t add) +{ + if (!m) + return false; + if (add > SIZE_MAX - m->size) + return false; + size_t need = m->size + add; + if (need <= m->cap) + return true; + + size_t new_cap = (m->cap > 0u) ? m->cap : 256u; + while (new_cap < need) + { + if (new_cap > (SIZE_MAX / 2u)) + { + new_cap = need; + break; + } + new_cap *= 2u; + } + + uint8_t *p = (uint8_t *)IKV_REALLOC(m->data, new_cap); + if (!p) + return false; + m->data = p; + m->cap = new_cap; + return true; +} + +static void bw_mem_write_bytes(void *ctx, bool *ok, const void *p, size_t n) +{ + bw_mem_t *m = (bw_mem_t *)ctx; + if (!m || !ok || !*ok) + return; + if (n == 0u) + return; + if (!bw_mem_reserve(m, n)) + { + *ok = false; + return; + } + memcpy(m->data + m->size, p, n); + m->size += n; +} + +static void bw_bytes(bw_t *w, const void *p, size_t n) +{ + if (!w || !w->ok || !w->write_bytes_fn) + return; + w->write_bytes_fn(w->ctx, &w->ok, p, n); +} + +static void bw_u8(bw_t *w, uint8_t v) { bw_bytes(w, &v, 1u); } + +static void bw_u32le(bw_t *w, uint32_t v) +{ + uint8_t b[4]; + b[0] = (uint8_t)(v & 255u); + b[1] = (uint8_t)((v >> 8) & 255u); + b[2] = (uint8_t)((v >> 16) & 255u); + b[3] = (uint8_t)((v >> 24) & 255u); + bw_bytes(w, b, sizeof(b)); +} + +static void bw_u64le(bw_t *w, uint64_t v) +{ + uint8_t b[8]; + b[0] = (uint8_t)(v & 255u); + b[1] = (uint8_t)((v >> 8) & 255u); + b[2] = (uint8_t)((v >> 16) & 255u); + b[3] = (uint8_t)((v >> 24) & 255u); + b[4] = (uint8_t)((v >> 32) & 255u); + b[5] = (uint8_t)((v >> 40) & 255u); + b[6] = (uint8_t)((v >> 48) & 255u); + b[7] = (uint8_t)((v >> 56) & 255u); + bw_bytes(w, b, sizeof(b)); +} + +static void bw_varu64(bw_t *w, uint64_t v) +{ + while (v >= 0x80u) + { + bw_u8(w, (uint8_t)((v & 0x7Fu) | 0x80u)); + v >>= 7; + } + bw_u8(w, (uint8_t)v); +} + +static void bw_varu32(bw_t *w, uint32_t v) { bw_varu64(w, (uint64_t)v); } + +static uint64_t bw_zz64(int64_t v) +{ + return (uint64_t)((v << 1) ^ (v >> 63)); +} + +static void bw_vari64(bw_t *w, int64_t v) { bw_varu64(w, bw_zz64(v)); } + +static void bw_str(bw_t *w, const char *s) +{ + const char *p = s ? s : ""; + size_t n = strlen(p); + if (n > 0xFFFFFFFFu) + n = 0xFFFFFFFFu; + bw_varu32(w, (uint32_t)n); + bw_bytes(w, p, n); +} + +static uint32_t bw_object_count(const ikv_node_t *obj) +{ + uint32_t c = 0; + for (uint32_t i = 0; i < obj->value.object.bucket_count; i++) + for (ikv_node_t *n = obj->value.object.buckets[i]; n; n = n->next) + c++; + return c; +} + +static void bw_val_payload(bw_t *w, const ikv_node_t *n); + +static void bw_node(bw_t *w, const ikv_node_t *n) +{ + bw_u8(w, (uint8_t)n->type); + bw_val_payload(w, n); +} + +static void bw_fixed_payload(bw_t *w, const ikv_node_t *n, ikv_type_t t) +{ + if (!n) + { + if (t == IKV_STRING) + bw_str(w, ""); + else if (t == IKV_INT) + bw_vari64(w, 0); + else if (t == IKV_BOOL) + bw_u8(w, 0); + else if (t == IKV_FLOAT) + { + union + { + double d; + uint64_t u; + } x; + x.d = 0.0; + bw_u64le(w, x.u); + } + return; + } + + if (t == IKV_STRING) + { + bw_str(w, (n->type == IKV_STRING) ? n->value.string : ""); + return; + } + if (t == IKV_INT) + { + bw_vari64(w, (n->type == IKV_INT) ? n->value.i : 0); + return; + } + if (t == IKV_BOOL) + { + bw_u8(w, (uint8_t)((n->type == IKV_BOOL) ? (n->value.b ? 1 : 0) : 0)); + return; + } + if (t == IKV_FLOAT) + { + union + { + double d; + uint64_t u; + } x; + x.d = (n->type == IKV_FLOAT) ? n->value.f : 0.0; + bw_u64le(w, x.u); + return; + } + bw_node(w, n); +} + +static void bw_val_payload(bw_t *w, const ikv_node_t *n) +{ + switch (n->type) + { + case IKV_NULL: + break; + case IKV_BOOL: + bw_u8(w, (uint8_t)(n->value.b ? 1 : 0)); + break; + case IKV_INT: + bw_vari64(w, n->value.i); + break; + case IKV_FLOAT: + { + union + { + double d; + uint64_t u; + } x; + x.d = n->value.f; + bw_u64le(w, x.u); + } + break; + case IKV_STRING: + bw_str(w, n->value.string); + break; + case IKV_OBJECT: + { + uint32_t cnt = bw_object_count(n); + bw_varu32(w, cnt); + for (uint32_t i = 0; i < n->value.object.bucket_count; i++) + { + for (ikv_node_t *c = n->value.object.buckets[i]; c; c = c->next) + { + bw_str(w, c->key ? c->key : ""); + bw_node(w, c); + } + } + } + break; + case IKV_ARRAY: + { + bw_u8(w, (uint8_t)n->value.array.element_type); + bw_varu32(w, n->value.array.count); + ikv_type_t et = n->value.array.element_type; + if (et == IKV_NULL) + { + for (uint32_t i = 0; i < n->value.array.count; i++) + bw_node(w, n->value.array.items[i]); + } + else + { + for (uint32_t i = 0; i < n->value.array.count; i++) + bw_fixed_payload(w, n->value.array.items[i], et); + } + } + break; + default: + break; + } +} + +bool ikv__write_binary_memory_version(const ikv_node_t *root, uint8_t **out_data, uint32_t *out_size, uint32_t version) +{ + if (!root || !out_data || !out_size) + return false; + + *out_data = NULL; + *out_size = 0u; + + bw_mem_t mem = {0}; + bw_t w = {0}; + w.ctx = &mem; + w.ok = true; + w.write_bytes_fn = bw_mem_write_bytes; + + uint8_t magic[4]; + magic[0] = (uint8_t)'i'; + magic[1] = (uint8_t)'K'; + magic[2] = (uint8_t)'v'; + magic[3] = (uint8_t)('0' + (uint8_t)version); + bw_bytes(&w, magic, sizeof(magic)); + + bw_u8(&w, (uint8_t)'b'); + bw_u32le(&w, version); + bw_str(&w, (root->key && root->key[0]) ? root->key : "root"); + bw_node(&w, root); + + if (!w.ok || mem.size == 0u || mem.size > 0xFFFFFFFFu) + { + IKV_FREE(mem.data); + return false; + } + + *out_data = mem.data; + *out_size = (uint32_t)mem.size; + return true; +} + +bool ikv__write_binary_node_memory(const ikv_node_t *node, uint8_t **out_data, uint32_t *out_size) +{ + bw_mem_t mem = {0}; + bw_t w = {0}; + + if (!node || !out_data || !out_size) + return false; + + *out_data = NULL; + *out_size = 0u; + + w.ctx = &mem; + w.ok = true; + w.write_bytes_fn = bw_mem_write_bytes; + bw_node(&w, node); + + if (!w.ok || mem.size == 0u || mem.size > 0xFFFFFFFFu) + { + IKV_FREE(mem.data); + return false; + } + + *out_data = mem.data; + *out_size = (uint32_t)mem.size; + return true; +} + +bool ikv__write_binary_file_version(const char *path, const ikv_node_t *root, uint32_t version) +{ + if (!path || !root) + return false; + + uint8_t *data = NULL; + uint32_t size = 0u; + if (!ikv__write_binary_memory_version(root, &data, &size, version) || !data || size == 0u) + return false; + + FILE *f = fopen(path, "wb"); + if (!f) + { + IKV_FREE(data); + return false; + } + + bool ok = (fwrite(data, 1, size, f) == size); + if (fclose(f) != 0) + ok = false; + IKV_FREE(data); + return ok; +} + +typedef struct +{ + const uint8_t *p; + const uint8_t *e; +} br_t; + +static bool br_need(br_t *r, size_t n) { return r && r->p && r->e && (size_t)(r->e - r->p) >= n; } + +static bool br_u8(br_t *r, uint8_t *out) +{ + if (!br_need(r, 1)) + return false; + *out = r->p[0]; + r->p += 1; + return true; +} + +static bool br_u32le(br_t *r, uint32_t *out) +{ + if (!br_need(r, 4)) + return false; + const uint8_t *p = r->p; + *out = (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); + r->p += 4; + return true; +} + +static bool br_u64le(br_t *r, uint64_t *out) +{ + if (!br_need(r, 8)) + return false; + const uint8_t *p = r->p; + *out = (uint64_t)p[0] | + ((uint64_t)p[1] << 8) | + ((uint64_t)p[2] << 16) | + ((uint64_t)p[3] << 24) | + ((uint64_t)p[4] << 32) | + ((uint64_t)p[5] << 40) | + ((uint64_t)p[6] << 48) | + ((uint64_t)p[7] << 56); + r->p += 8; + return true; +} + +static bool br_varu64(br_t *r, uint64_t *out) +{ + uint64_t v = 0; + uint32_t s = 0; + for (;;) + { + uint8_t b = 0; + if (!br_u8(r, &b)) + return false; + v |= (uint64_t)(b & 0x7Fu) << s; + if ((b & 0x80u) == 0) + break; + s += 7; + if (s > 63) + return false; + } + *out = v; + return true; +} + +static bool br_varu32(br_t *r, uint32_t *out) +{ + uint64_t v = 0; + if (!br_varu64(r, &v)) + return false; + if (v > 0xFFFFFFFFu) + return false; + *out = (uint32_t)v; + return true; +} + +static int64_t br_zzd64(uint64_t v) +{ + return (int64_t)((v >> 1) ^ (uint64_t)-(int64_t)(v & 1u)); +} + +static bool br_vari64(br_t *r, int64_t *out) +{ + uint64_t v = 0; + if (!br_varu64(r, &v)) + return false; + *out = br_zzd64(v); + return true; +} + +static char *br_str(br_t *r) +{ + uint32_t n = 0; + if (!br_varu32(r, &n)) + return NULL; + if (!br_need(r, n)) + return NULL; + char *s = (char *)IKV_MALLOC((size_t)n + 1); + if (!s) + return NULL; + if (n) + memcpy(s, r->p, n); + s[n] = 0; + r->p += n; + return s; +} + +static ikv_node_t *br_node(br_t *r); + +static ikv_node_t *br_fixed_value(br_t *r, ikv_type_t t) +{ + ikv_node_t *n = (ikv_node_t *)IKV_CALLOC(1, sizeof(*n)); + if (!n) + return NULL; + n->type = t; + + if (t == IKV_STRING) + { + n->value.string = br_str(r); + if (!n->value.string) + { + ikv_free(n); + return NULL; + } + return n; + } + if (t == IKV_INT) + { + int64_t v = 0; + if (!br_vari64(r, &v)) + { + ikv_free(n); + return NULL; + } + n->value.i = v; + return n; + } + if (t == IKV_BOOL) + { + uint8_t b = 0; + if (!br_u8(r, &b)) + { + ikv_free(n); + return NULL; + } + n->value.b = (b != 0); + return n; + } + if (t == IKV_FLOAT) + { + uint64_t u = 0; + if (!br_u64le(r, &u)) + { + ikv_free(n); + return NULL; + } + union + { + uint64_t u; + double d; + } x; + x.u = u; + n->value.f = x.d; + return n; + } + + ikv_free(n); + return br_node(r); +} + +static ikv_node_t *br_node(br_t *r) +{ + uint8_t ty = 0; + if (!br_u8(r, &ty)) + return NULL; + + ikv_node_t *n = (ikv_node_t *)IKV_CALLOC(1, sizeof(*n)); + if (!n) + return NULL; + n->type = (ikv_type_t)ty; + + if (n->type == IKV_NULL) + return n; + + if (n->type == IKV_BOOL) + { + uint8_t b = 0; + if (!br_u8(r, &b)) + { + ikv_free(n); + return NULL; + } + n->value.b = (b != 0); + return n; + } + + if (n->type == IKV_INT) + { + int64_t v = 0; + if (!br_vari64(r, &v)) + { + ikv_free(n); + return NULL; + } + n->value.i = v; + return n; + } + + if (n->type == IKV_FLOAT) + { + uint64_t u = 0; + if (!br_u64le(r, &u)) + { + ikv_free(n); + return NULL; + } + union + { + uint64_t u; + double d; + } x; + x.u = u; + n->value.f = x.d; + return n; + } + + if (n->type == IKV_STRING) + { + n->value.string = br_str(r); + if (!n->value.string) + { + ikv_free(n); + return NULL; + } + return n; + } + + if (n->type == IKV_OBJECT) + { + object_init(&n->value.object, 64); + if (!n->value.object.buckets) + { + ikv_free(n); + return NULL; + } + + uint32_t cnt = 0; + if (!br_varu32(r, &cnt)) + { + ikv_free(n); + return NULL; + } + + for (uint32_t i = 0; i < cnt; i++) + { + char *k = br_str(r); + if (!k) + { + ikv_free(n); + return NULL; + } + + ikv_node_t *v = br_node(r); + if (!v) + { + IKV_FREE(k); + ikv_free(n); + return NULL; + } + + IKV_FREE(v->key); + v->key = k; + object_set_node(&n->value.object, v); + } + + return n; + } + + if (n->type == IKV_ARRAY) + { + uint8_t et = 0; + if (!br_u8(r, &et)) + { + ikv_free(n); + return NULL; + } + n->value.array.element_type = (ikv_type_t)et; + + uint32_t cnt = 0; + if (!br_varu32(r, &cnt)) + { + ikv_free(n); + return NULL; + } + + n->value.array.count = 0; + n->value.array.items = NULL; + + if (cnt) + { + n->value.array.items = (ikv_node_t **)IKV_MALLOC(sizeof(ikv_node_t *) * (size_t)cnt); + if (!n->value.array.items) + { + ikv_free(n); + return NULL; + } + } + + if (n->value.array.element_type == IKV_NULL) + { + for (uint32_t i = 0; i < cnt; i++) + { + ikv_node_t *it = br_node(r); + if (!it) + { + ikv_free(n); + return NULL; + } + n->value.array.items[n->value.array.count++] = it; + } + } + else + { + ikv_type_t t = n->value.array.element_type; + for (uint32_t i = 0; i < cnt; i++) + { + ikv_node_t *it = br_fixed_value(r, t); + if (!it) + { + ikv_free(n); + return NULL; + } + n->value.array.items[n->value.array.count++] = it; + } + } + + return n; + } + + ikv_free(n); + return NULL; +} + +ikv_node_t *ikv__parse_binary_memory_version(const void *data, size_t size, uint32_t version) +{ + if (!data || size < 4 + 1 + 4) + return NULL; + + const uint8_t *p = (const uint8_t *)data; + + if (p[0] != (uint8_t)'i' || p[1] != (uint8_t)'K' || p[2] != (uint8_t)'v') + return NULL; + + uint8_t file_ver_digit = p[3]; + uint8_t want_digit = (uint8_t)('0' + (uint8_t)version); + if (file_ver_digit != want_digit) + return NULL; + + p += 4; + + if (*p++ != (uint8_t)'b') + return NULL; + + br_t r; + r.p = p; + r.e = (const uint8_t *)data + size; + + uint32_t ver = 0; + if (!br_u32le(&r, &ver)) + return NULL; + + if (ver != version) + return NULL; + + char *root_name = br_str(&r); + if (!root_name) + return NULL; + + ikv_node_t *root = br_node(&r); + if (!root) + { + IKV_FREE(root_name); + return NULL; + } + + IKV_FREE(root->key); + root->key = root_name; + return root; +} + +ikv_node_t *ikv__parse_binary_node_memory(const void *data, size_t size, size_t *out_consumed) +{ + br_t r; + ikv_node_t *node = NULL; + + if (!data || size == 0u) + return NULL; + + r.p = (const uint8_t *)data; + r.e = r.p + size; + + node = br_node(&r); + if (!node) + return NULL; + + if (out_consumed) + *out_consumed = (size_t)(r.p - (const uint8_t *)data); + + return node; +} + +ikv_node_t *ikv__parse_binary_file_version(const char *path, uint32_t version) +{ + if (!path) + return NULL; + + uint8_t *buf = NULL; + size_t size = 0u; + ikv_node_t *root = NULL; + + if (!read_file_bytes(path, &buf, &size) || !buf) + return NULL; + + root = ikv__parse_binary_memory_version(buf, size, version); + IKV_FREE(buf); + return root; +} + +void ikv__object_attach_loaded_node(ikv_node_t *object_node, ikv_node_t *child_node) +{ + if (!object_node || object_node->type != IKV_OBJECT) + return; + object_attach_loaded_node(&object_node->value.object, child_node); +} + +bool ikv__object_reserve_for_count(ikv_node_t *object_node, uint32_t entry_count) +{ + uint32_t bucket_count = 64u; + + if (!object_node || object_node->type != IKV_OBJECT) + return false; + + while (bucket_count < (entry_count * 4u) / 3u + 1u) + { + if (bucket_count > 0x7FFFFFFFu) + break; + bucket_count *= 2u; + } + + IKV_FREE(object_node->value.object.buckets); + object_node->value.object.bucket_count = bucket_count; + object_node->value.object.size = 0u; + object_node->value.object.buckets = (ikv_node_t **)IKV_CALLOC(bucket_count, sizeof(ikv_node_t *)); + return object_node->value.object.buckets != NULL; +} + +ikv_version_t ikv_detect_text_version(const char *src) +{ + if (!src) + return IKV_VERSION_UNKNOWN; + + while (*src && isspace((unsigned char)*src)) + src++; + + if (strncmp(src, "ikv1", 4) == 0) + return IKV_VERSION_1; + if (strncmp(src, "ikv2", 4) == 0) + return IKV_VERSION_2; + return IKV_VERSION_UNKNOWN; +} + +ikv_version_t ikv_detect_binary_version(const void *data, size_t size) +{ + const uint8_t *p = (const uint8_t *)data; + + if (!p || size < 9) + return IKV_VERSION_UNKNOWN; + if (p[0] != (uint8_t)'i' || p[1] != (uint8_t)'K' || p[2] != (uint8_t)'v') + return IKV_VERSION_UNKNOWN; + if (p[4] != (uint8_t)'b') + return IKV_VERSION_UNKNOWN; + if (p[3] == (uint8_t)'1') + return IKV_VERSION_1; + if (p[3] == (uint8_t)'2') + return IKV_VERSION_2; + return IKV_VERSION_UNKNOWN; +} + +ikv_version_t ikv_detect_file_version(const char *path, bool binary) +{ + uint8_t prefix[16] = {0}; + size_t size = 0; + FILE *file = NULL; + ikv_version_t version = IKV_VERSION_UNKNOWN; + + if (!path) + return IKV_VERSION_UNKNOWN; + + file = fopen(path, "rb"); + if (!file) + return IKV_VERSION_UNKNOWN; + + size = fread(prefix, 1u, sizeof(prefix) - 1u, file); + fclose(file); + + version = binary ? ikv_detect_binary_version(prefix, size) + : ikv_detect_text_version((const char *)prefix); + return version; +} + +bool ikv_write_file(const char *path, const ikv_node_t *root) +{ + return ikv_write_file_version(path, root, (ikv_version_t)IKV_CURRENT_VERSION); +} + +bool ikv_write_file_version(const char *path, const ikv_node_t *root, ikv_version_t version) +{ + const ikv_loader_t *loader = ikv_find_loader(version); + + if (!loader || !loader->write_text_file) + return false; + + return loader->write_text_file(path, root); +} + +ikv_node_t *ikv_parse_string(const char *src) +{ + ikv_version_t version = ikv_detect_text_version(src); + + if (version == IKV_VERSION_UNKNOWN) + version = IKV_VERSION_1; + + return ikv_parse_string_version(src, version); +} + +ikv_node_t *ikv_parse_string_version(const char *src, ikv_version_t version) +{ + const ikv_loader_t *loader = ikv_find_loader(version); + + if (!loader || !loader->parse_text_string) + return NULL; + + return loader->parse_text_string(src); +} + +ikv_node_t *ikv_parse_file(const char *path) +{ + ikv_version_t version = IKV_VERSION_UNKNOWN; + + version = ikv_detect_file_version(path, true); + if (version != IKV_VERSION_UNKNOWN) + return ikvb_parse_file_version(path, version); + + version = ikv_detect_file_version(path, false); + if (version == IKV_VERSION_UNKNOWN) + version = IKV_VERSION_1; + return ikv_parse_file_version(path, version); +} + +ikv_node_t *ikv_parse_file_version(const char *path, ikv_version_t version) +{ + const ikv_loader_t *loader = ikv_find_loader(version); + + if (!loader || !loader->parse_text_file) + return NULL; + + return loader->parse_text_file(path); +} + +bool ikvb_write_memory(const ikv_node_t *root, uint8_t **out_data, uint32_t *out_size) +{ + return ikvb_write_memory_version(root, out_data, out_size, (ikv_version_t)IKV_CURRENT_VERSION); +} + +bool ikvb_write_memory_version(const ikv_node_t *root, uint8_t **out_data, uint32_t *out_size, ikv_version_t version) +{ + const ikv_loader_t *loader = ikv_find_loader(version); + + if (!loader || !loader->write_binary_memory) + return false; + + return loader->write_binary_memory(root, out_data, out_size); +} + +bool ikvb_write_file(const char *path, const ikv_node_t *root) +{ + return ikvb_write_file_version(path, root, (ikv_version_t)IKV_CURRENT_VERSION); +} + +bool ikvb_write_file_version(const char *path, const ikv_node_t *root, ikv_version_t version) +{ + const ikv_loader_t *loader = ikv_find_loader(version); + + if (!loader || !loader->write_binary_file) + return false; + + return loader->write_binary_file(path, root); +} + +ikv_node_t *ikvb_parse_memory(const void *data, size_t size) +{ + ikv_version_t version = ikv_detect_binary_version(data, size); + + return ikvb_parse_memory_version(data, size, version); +} + +ikv_node_t *ikvb_parse_memory_version(const void *data, size_t size, ikv_version_t version) +{ + const ikv_loader_t *loader = ikv_find_loader(version); + + if (!loader || !loader->parse_binary_memory) + return NULL; + + return loader->parse_binary_memory(data, size); +} + +ikv_node_t *ikvb_parse_file(const char *path) +{ + ikv_version_t version = ikv_detect_file_version(path, true); + + if (version == IKV_VERSION_UNKNOWN) + return NULL; + return ikvb_parse_file_version(path, version); +} + +ikv_node_t *ikvb_parse_file_version(const char *path, ikv_version_t version) +{ + const ikv_loader_t *loader = ikv_find_loader(version); + + if (!loader || !loader->parse_binary_file) + return NULL; + + return loader->parse_binary_file(path); +} diff --git a/src/internal/ikv_internal.h b/src/internal/ikv_internal.h new file mode 100644 index 0000000..87d4025 --- /dev/null +++ b/src/internal/ikv_internal.h @@ -0,0 +1,66 @@ +#pragma once + +#include "../../include/ikv.h" + +typedef struct +{ + ikv_type_t element_type; + uint32_t count; + ikv_node_t **items; +} ikv_array_t; + +typedef struct +{ + uint32_t bucket_count; + uint32_t size; + ikv_node_t **buckets; +} ikv_object_t; + +typedef struct ikv_lazy_state_t +{ + void (*destroy)(struct ikv_lazy_state_t *state); + ikv_node_t *(*load_object_key)(struct ikv_lazy_state_t *state, ikv_node_t *object_node, const char *key); +} ikv_lazy_state_t; + +struct ikv_node_t +{ + char *key; + ikv_type_t type; + ikv_lazy_state_t *lazy_state; + union + { + char *string; + int64_t i; + double f; + bool b; + ikv_object_t object; + ikv_array_t array; + } value; + ikv_node_t *next; +}; + +typedef struct ikv_loader_t +{ + ikv_version_t version; + bool (*write_text_file)(const char *path, const ikv_node_t *root); + ikv_node_t *(*parse_text_file)(const char *path); + ikv_node_t *(*parse_text_string)(const char *src); + bool (*write_binary_file)(const char *path, const ikv_node_t *root); + bool (*write_binary_memory)(const ikv_node_t *root, uint8_t **out_data, uint32_t *out_size); + ikv_node_t *(*parse_binary_file)(const char *path); + ikv_node_t *(*parse_binary_memory)(const void *data, size_t size); +} ikv_loader_t; + +bool ikv__write_text_file_version(const char *path, const ikv_node_t *root, uint32_t version); +ikv_node_t *ikv__parse_text_file_version(const char *path, uint32_t version); +ikv_node_t *ikv__parse_text_string_version(const char *src, uint32_t version); + +bool ikv__write_binary_file_version(const char *path, const ikv_node_t *root, uint32_t version); +bool ikv__write_binary_memory_version(const ikv_node_t *root, uint8_t **out_data, uint32_t *out_size, uint32_t version); +ikv_node_t *ikv__parse_binary_file_version(const char *path, uint32_t version); +ikv_node_t *ikv__parse_binary_memory_version(const void *data, size_t size, uint32_t version); + +bool ikv__write_binary_node_memory(const ikv_node_t *node, uint8_t **out_data, uint32_t *out_size); +ikv_node_t *ikv__parse_binary_node_memory(const void *data, size_t size, size_t *out_consumed); +void ikv__object_attach_loaded_node(ikv_node_t *object_node, ikv_node_t *child_node); +bool ikv__object_reserve_for_count(ikv_node_t *object_node, uint32_t entry_count); diff --git a/src/loaders/ikv1.c b/src/loaders/ikv1.c new file mode 100644 index 0000000..e4f6e3d --- /dev/null +++ b/src/loaders/ikv1.c @@ -0,0 +1,47 @@ +#include "ikv1.h" + +static bool ikv1_write_text_file(const char *path, const ikv_node_t *root) +{ + return ikv__write_text_file_version(path, root, IKV_V1); +} + +static ikv_node_t *ikv1_parse_text_file(const char *path) +{ + return ikv__parse_text_file_version(path, IKV_V1); +} + +static ikv_node_t *ikv1_parse_text_string(const char *src) +{ + return ikv__parse_text_string_version(src, IKV_V1); +} + +static bool ikv1_write_binary_file(const char *path, const ikv_node_t *root) +{ + return ikv__write_binary_file_version(path, root, IKV_V1); +} + +static bool ikv1_write_binary_memory(const ikv_node_t *root, uint8_t **out_data, uint32_t *out_size) +{ + return ikv__write_binary_memory_version(root, out_data, out_size, IKV_V1); +} + +static ikv_node_t *ikv1_parse_binary_file(const char *path) +{ + return ikv__parse_binary_file_version(path, IKV_V1); +} + +static ikv_node_t *ikv1_parse_binary_memory(const void *data, size_t size) +{ + return ikv__parse_binary_memory_version(data, size, IKV_V1); +} + +const ikv_loader_t ikv_loader_v1 = { + IKV_VERSION_1, + ikv1_write_text_file, + ikv1_parse_text_file, + ikv1_parse_text_string, + ikv1_write_binary_file, + ikv1_write_binary_memory, + ikv1_parse_binary_file, + ikv1_parse_binary_memory, +}; diff --git a/src/loaders/ikv1.h b/src/loaders/ikv1.h new file mode 100644 index 0000000..1a4ad80 --- /dev/null +++ b/src/loaders/ikv1.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../internal/ikv_internal.h" + +extern const ikv_loader_t ikv_loader_v1; diff --git a/src/loaders/ikv2.c b/src/loaders/ikv2.c new file mode 100644 index 0000000..25f3e8a --- /dev/null +++ b/src/loaders/ikv2.c @@ -0,0 +1,810 @@ +#include "ikv2.h" + +#include +#include +#include + +#define IKV2_BINARY_FLAGS_INDEXED_ROOT 1u + +typedef struct +{ + char *key; + uint8_t type; + uint32_t payload_offset; + uint32_t payload_size; + uint8_t *payload_data; + bool loaded; +} ikv2_index_entry_t; + +typedef enum +{ + IKV2_SOURCE_FILE = 1, + IKV2_SOURCE_MEMORY = 2 +} ikv2_source_kind_t; + +typedef struct +{ + ikv_lazy_state_t base; + ikv2_source_kind_t source_kind; + char *file_path; + uint8_t *memory_data; + size_t memory_size; + uint32_t entry_count; + ikv2_index_entry_t *entries; +} ikv2_lazy_root_t; + +typedef struct +{ + uint8_t *data; + size_t size; + size_t capacity; + bool ok; +} ikv2_buffer_t; + +typedef struct +{ + const uint8_t *data; + size_t size; + size_t offset; +} ikv2_cursor_t; + +static bool ikv2_buffer_reserve(ikv2_buffer_t *buffer, size_t additional) +{ + uint8_t *next = NULL; + size_t required = 0; + size_t capacity = 0; + + if (!buffer || !buffer->ok) + return false; + + required = buffer->size + additional; + if (required <= buffer->capacity) + return true; + + capacity = buffer->capacity ? buffer->capacity : 256u; + while (capacity < required) + capacity *= 2u; + + next = (uint8_t *)realloc(buffer->data, capacity); + if (!next) + { + buffer->ok = false; + return false; + } + + buffer->data = next; + buffer->capacity = capacity; + return true; +} + +static char *ikv2_strdup(const char *value) +{ + size_t length = 0u; + char *copy = NULL; + + if (!value) + value = ""; + + length = strlen(value); + copy = (char *)malloc(length + 1u); + if (!copy) + return NULL; + + memcpy(copy, value, length + 1u); + return copy; +} + +static void ikv2_buffer_write_bytes(ikv2_buffer_t *buffer, const void *data, size_t size) +{ + if (!ikv2_buffer_reserve(buffer, size)) + return; + + if (size > 0u) + memcpy(buffer->data + buffer->size, data, size); + buffer->size += size; +} + +static void ikv2_buffer_write_u8(ikv2_buffer_t *buffer, uint8_t value) +{ + ikv2_buffer_write_bytes(buffer, &value, 1u); +} + +static void ikv2_buffer_write_u32le(ikv2_buffer_t *buffer, uint32_t value) +{ + uint8_t bytes[4]; + bytes[0] = (uint8_t)(value & 0xFFu); + bytes[1] = (uint8_t)((value >> 8) & 0xFFu); + bytes[2] = (uint8_t)((value >> 16) & 0xFFu); + bytes[3] = (uint8_t)((value >> 24) & 0xFFu); + ikv2_buffer_write_bytes(buffer, bytes, sizeof(bytes)); +} + +static void ikv2_buffer_write_varu32(ikv2_buffer_t *buffer, uint32_t value) +{ + while (value >= 0x80u) + { + ikv2_buffer_write_u8(buffer, (uint8_t)((value & 0x7Fu) | 0x80u)); + value >>= 7; + } + + ikv2_buffer_write_u8(buffer, (uint8_t)value); +} + +static void ikv2_buffer_write_string(ikv2_buffer_t *buffer, const char *value) +{ + size_t length = value ? strlen(value) : 0u; + ikv2_buffer_write_varu32(buffer, (uint32_t)length); + if (length > 0u) + ikv2_buffer_write_bytes(buffer, value, length); +} + +static bool ikv2_cursor_need(const ikv2_cursor_t *cursor, size_t size) +{ + return cursor && cursor->offset + size <= cursor->size; +} + +static bool ikv2_cursor_read_u8(ikv2_cursor_t *cursor, uint8_t *out_value) +{ + if (!ikv2_cursor_need(cursor, 1u)) + return false; + *out_value = cursor->data[cursor->offset++]; + return true; +} + +static bool ikv2_cursor_read_u32le(ikv2_cursor_t *cursor, uint32_t *out_value) +{ + const uint8_t *p = NULL; + + if (!ikv2_cursor_need(cursor, 4u)) + return false; + + p = cursor->data + cursor->offset; + *out_value = (uint32_t)p[0] | + ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | + ((uint32_t)p[3] << 24); + cursor->offset += 4u; + return true; +} + +static bool ikv2_cursor_read_varu32(ikv2_cursor_t *cursor, uint32_t *out_value) +{ + uint32_t shift = 0u; + uint32_t value = 0u; + + while (shift < 32u) + { + uint8_t byte = 0u; + if (!ikv2_cursor_read_u8(cursor, &byte)) + return false; + + value |= (uint32_t)(byte & 0x7Fu) << shift; + if ((byte & 0x80u) == 0u) + { + *out_value = value; + return true; + } + + shift += 7u; + } + + return false; +} + +static char *ikv2_cursor_read_string(ikv2_cursor_t *cursor) +{ + uint32_t length = 0u; + char *value = NULL; + + if (!ikv2_cursor_read_varu32(cursor, &length)) + return NULL; + if (!ikv2_cursor_need(cursor, (size_t)length)) + return NULL; + + value = (char *)malloc((size_t)length + 1u); + if (!value) + return NULL; + + if (length > 0u) + memcpy(value, cursor->data + cursor->offset, length); + value[length] = 0; + cursor->offset += (size_t)length; + return value; +} + +static bool ikv2_file_read_u8(FILE *file, uint8_t *out_value) +{ + return file && out_value && fread(out_value, 1u, 1u, file) == 1u; +} + +static bool ikv2_file_read_u32le(FILE *file, uint32_t *out_value) +{ + uint8_t bytes[4]; + + if (!file || !out_value || fread(bytes, 1u, sizeof(bytes), file) != sizeof(bytes)) + return false; + + *out_value = (uint32_t)bytes[0] | + ((uint32_t)bytes[1] << 8) | + ((uint32_t)bytes[2] << 16) | + ((uint32_t)bytes[3] << 24); + return true; +} + +static bool ikv2_file_read_varu32(FILE *file, uint32_t *out_value) +{ + uint32_t shift = 0u; + uint32_t value = 0u; + + while (shift < 32u) + { + uint8_t byte = 0u; + if (!ikv2_file_read_u8(file, &byte)) + return false; + + value |= (uint32_t)(byte & 0x7Fu) << shift; + if ((byte & 0x80u) == 0u) + { + *out_value = value; + return true; + } + + shift += 7u; + } + + return false; +} + +static char *ikv2_file_read_string(FILE *file) +{ + uint32_t length = 0u; + char *value = NULL; + + if (!ikv2_file_read_varu32(file, &length)) + return NULL; + + value = (char *)malloc((size_t)length + 1u); + if (!value) + return NULL; + + if (length > 0u && fread(value, 1u, length, file) != length) + { + free(value); + return NULL; + } + + value[length] = 0; + return value; +} + +static int ikv2_compare_entries(const void *lhs, const void *rhs) +{ + const ikv2_index_entry_t *left = (const ikv2_index_entry_t *)lhs; + const ikv2_index_entry_t *right = (const ikv2_index_entry_t *)rhs; + return strcmp(left->key ? left->key : "", right->key ? right->key : ""); +} + +static bool ikv2_collect_root_entries(const ikv_node_t *root, ikv2_index_entry_t **out_entries, uint32_t *out_count) +{ + ikv2_index_entry_t *entries = NULL; + uint32_t count = 0u; + uint32_t index = 0u; + + if (!root || root->type != IKV_OBJECT || !out_entries || !out_count) + return false; + + count = root->value.object.size; + entries = count ? (ikv2_index_entry_t *)calloc(count, sizeof(*entries)) : NULL; + if (count > 0u && !entries) + return false; + + for (uint32_t bucket = 0; bucket < root->value.object.bucket_count; ++bucket) + { + for (ikv_node_t *node = root->value.object.buckets[bucket]; node; node = node->next) + { + uint8_t *payload = NULL; + uint32_t payload_size = 0u; + + if (index >= count || !ikv__write_binary_node_memory(node, &payload, &payload_size)) + { + if (payload) + free(payload); + for (uint32_t i = 0; i < index; ++i) + { + free(entries[i].key); + free(entries[i].payload_data); + } + free(entries); + return false; + } + + entries[index].key = ikv2_strdup(node->key ? node->key : ""); + entries[index].type = (uint8_t)node->type; + entries[index].payload_data = payload; + entries[index].payload_size = payload_size; + entries[index].loaded = false; + if (!entries[index].key) + { + free(payload); + for (uint32_t i = 0; i < index; ++i) + { + free(entries[i].key); + free(entries[i].payload_data); + } + free(entries); + return false; + } + + ++index; + } + } + + qsort(entries, count, sizeof(*entries), ikv2_compare_entries); + *out_entries = entries; + *out_count = count; + return true; +} + +static bool ikv2_build_indexed_binary(const ikv_node_t *root, uint8_t **out_data, uint32_t *out_size) +{ + ikv2_index_entry_t *entries = NULL; + uint32_t entry_count = 0u; + uint32_t header_size = 0u; + uint32_t payload_base = 0u; + ikv2_buffer_t buffer = {0}; + + if (!root || !out_data || !out_size || root->type != IKV_OBJECT) + return false; + + buffer.ok = true; + *out_data = NULL; + *out_size = 0u; + + if (!ikv2_collect_root_entries(root, &entries, &entry_count)) + return false; + + header_size = 4u + 1u + 4u + 4u; + header_size += 1u + (uint32_t)strlen(root->key && root->key[0] ? root->key : "root"); + header_size += 1u; + for (uint32_t i = 0; i < entry_count; ++i) + header_size += 1u + (uint32_t)strlen(entries[i].key ? entries[i].key : ""); + header_size += entry_count * (1u + 4u + 4u); + payload_base = header_size; + + ikv2_buffer_write_bytes(&buffer, "iKv2", 4u); + ikv2_buffer_write_u8(&buffer, (uint8_t)'b'); + ikv2_buffer_write_u32le(&buffer, IKV_V2); + ikv2_buffer_write_u32le(&buffer, IKV2_BINARY_FLAGS_INDEXED_ROOT); + ikv2_buffer_write_string(&buffer, (root->key && root->key[0]) ? root->key : "root"); + ikv2_buffer_write_varu32(&buffer, entry_count); + + for (uint32_t i = 0; i < entry_count; ++i) + ikv2_buffer_write_string(&buffer, entries[i].key ? entries[i].key : ""); + + for (uint32_t i = 0; i < entry_count; ++i) + { + ikv2_buffer_write_u8(&buffer, entries[i].type); + ikv2_buffer_write_u32le(&buffer, payload_base); + ikv2_buffer_write_u32le(&buffer, entries[i].payload_size); + payload_base += entries[i].payload_size; + } + + for (uint32_t i = 0; i < entry_count; ++i) + { + const uint8_t *payload = entries[i].payload_data; + ikv2_buffer_write_bytes(&buffer, payload, entries[i].payload_size); + } + + for (uint32_t i = 0; i < entry_count; ++i) + { + free(entries[i].key); + free(entries[i].payload_data); + } + free(entries); + + if (!buffer.ok || buffer.size == 0u || buffer.size > 0xFFFFFFFFu) + { + free(buffer.data); + return false; + } + + *out_data = buffer.data; + *out_size = (uint32_t)buffer.size; + return true; +} + +static void ikv2_lazy_root_destroy(ikv_lazy_state_t *state) +{ + ikv2_lazy_root_t *lazy_root = (ikv2_lazy_root_t *)state; + + if (!lazy_root) + return; + + for (uint32_t i = 0; i < lazy_root->entry_count; ++i) + free(lazy_root->entries[i].key); + free(lazy_root->entries); + free(lazy_root->file_path); + free(lazy_root->memory_data); + free(lazy_root); +} + +static ikv2_index_entry_t *ikv2_find_entry(ikv2_lazy_root_t *lazy_root, const char *key) +{ + uint32_t left = 0u; + uint32_t right = 0u; + + if (!lazy_root || !key || lazy_root->entry_count == 0u) + return NULL; + + right = lazy_root->entry_count; + while (left < right) + { + uint32_t mid = left + (right - left) / 2u; + int compare = strcmp(key, lazy_root->entries[mid].key ? lazy_root->entries[mid].key : ""); + if (compare == 0) + return &lazy_root->entries[mid]; + if (compare < 0) + right = mid; + else + left = mid + 1u; + } + + return NULL; +} + +static bool ikv2_read_payload_from_file(const char *path, uint32_t offset, uint32_t size, uint8_t **out_data) +{ + FILE *file = NULL; + uint8_t *data = NULL; + + if (!path || !out_data) + return false; + + *out_data = NULL; + file = fopen(path, "rb"); + if (!file) + return false; + + if (fseek(file, (long)offset, SEEK_SET) != 0) + { + fclose(file); + return false; + } + + data = (uint8_t *)malloc(size); + if (!data) + { + fclose(file); + return false; + } + + if (size > 0u && fread(data, 1u, size, file) != size) + { + free(data); + fclose(file); + return false; + } + + fclose(file); + *out_data = data; + return true; +} + +static ikv_node_t *ikv2_lazy_root_load_object_key(ikv_lazy_state_t *state, ikv_node_t *object_node, const char *key) +{ + ikv2_lazy_root_t *lazy_root = (ikv2_lazy_root_t *)state; + ikv2_index_entry_t *entry = NULL; + const uint8_t *payload_data = NULL; + uint8_t *owned_payload = NULL; + ikv_node_t *node = NULL; + size_t consumed = 0u; + + (void)object_node; + + if (!lazy_root || !key) + return NULL; + + entry = ikv2_find_entry(lazy_root, key); + if (!entry) + return NULL; + + if (lazy_root->source_kind == IKV2_SOURCE_MEMORY) + { + if ((size_t)entry->payload_offset + (size_t)entry->payload_size > lazy_root->memory_size) + return NULL; + payload_data = lazy_root->memory_data + entry->payload_offset; + } + else + { + if (!ikv2_read_payload_from_file(lazy_root->file_path, entry->payload_offset, entry->payload_size, &owned_payload)) + return NULL; + payload_data = owned_payload; + } + + node = ikv__parse_binary_node_memory(payload_data, entry->payload_size, &consumed); + free(owned_payload); + if (!node || consumed != entry->payload_size) + { + if (node) + ikv_free(node); + return NULL; + } + + entry->loaded = true; + return node; +} + +static ikv_node_t *ikv2_parse_indexed_binary_buffer(uint8_t *buffer, size_t size) +{ + ikv2_cursor_t cursor; + uint32_t version = 0u; + uint32_t flags = 0u; + uint32_t entry_count = 0u; + uint8_t kind = 0u; + char *root_name = NULL; + ikv_node_t *root = NULL; + ikv2_lazy_root_t *lazy_root = NULL; + + cursor.data = buffer; + cursor.size = size; + cursor.offset = 0u; + + if (!ikv2_cursor_need(&cursor, 9u)) + return NULL; + if (memcmp(cursor.data, "iKv2", 4u) != 0) + return NULL; + cursor.offset += 4u; + if (!ikv2_cursor_read_u8(&cursor, &kind)) + return NULL; + if (kind != (uint8_t)'b') + return NULL; + if (!ikv2_cursor_read_u32le(&cursor, &version) || version != IKV_V2) + return NULL; + if (!ikv2_cursor_read_u32le(&cursor, &flags) || (flags & IKV2_BINARY_FLAGS_INDEXED_ROOT) == 0u) + return NULL; + + root_name = ikv2_cursor_read_string(&cursor); + if (!root_name) + return NULL; + if (!ikv2_cursor_read_varu32(&cursor, &entry_count)) + { + free(root_name); + return NULL; + } + + root = ikv_create_object(root_name); + free(root_name); + if (!root) + return NULL; + if (!ikv__object_reserve_for_count(root, entry_count)) + { + ikv_free(root); + return NULL; + } + root->value.object.size = entry_count; + + lazy_root = (ikv2_lazy_root_t *)calloc(1u, sizeof(*lazy_root)); + if (!lazy_root) + { + ikv_free(root); + return NULL; + } + + lazy_root->base.destroy = ikv2_lazy_root_destroy; + lazy_root->base.load_object_key = ikv2_lazy_root_load_object_key; + lazy_root->entry_count = entry_count; + lazy_root->entries = entry_count ? (ikv2_index_entry_t *)calloc(entry_count, sizeof(*lazy_root->entries)) : NULL; + if (entry_count > 0u && !lazy_root->entries) + { + ikv2_lazy_root_destroy(&lazy_root->base); + ikv_free(root); + return NULL; + } + + for (uint32_t i = 0; i < entry_count; ++i) + { + lazy_root->entries[i].key = ikv2_cursor_read_string(&cursor); + if (!lazy_root->entries[i].key) + { + ikv2_lazy_root_destroy(&lazy_root->base); + ikv_free(root); + return NULL; + } + } + + for (uint32_t i = 0; i < entry_count; ++i) + { + if (!ikv2_cursor_read_u8(&cursor, &lazy_root->entries[i].type) || + !ikv2_cursor_read_u32le(&cursor, &lazy_root->entries[i].payload_offset) || + !ikv2_cursor_read_u32le(&cursor, &lazy_root->entries[i].payload_size)) + { + ikv2_lazy_root_destroy(&lazy_root->base); + ikv_free(root); + return NULL; + } + } + + lazy_root->source_kind = IKV2_SOURCE_MEMORY; + lazy_root->memory_data = buffer; + lazy_root->memory_size = size; + + root->lazy_state = &lazy_root->base; + return root; +} + +static bool ikv2_write_text_file(const char *path, const ikv_node_t *root) +{ + return ikv__write_text_file_version(path, root, IKV_V2); +} + +static ikv_node_t *ikv2_parse_text_file(const char *path) +{ + return ikv__parse_text_file_version(path, IKV_V2); +} + +static ikv_node_t *ikv2_parse_text_string(const char *src) +{ + return ikv__parse_text_string_version(src, IKV_V2); +} + +static bool ikv2_write_binary_memory(const ikv_node_t *root, uint8_t **out_data, uint32_t *out_size) +{ + return ikv2_build_indexed_binary(root, out_data, out_size); +} + +static bool ikv2_write_binary_file(const char *path, const ikv_node_t *root) +{ + uint8_t *data = NULL; + uint32_t size = 0u; + FILE *file = NULL; + bool ok = false; + + if (!path || !ikv2_write_binary_memory(root, &data, &size)) + return false; + + file = fopen(path, "wb"); + if (!file) + { + free(data); + return false; + } + + ok = (fwrite(data, 1u, size, file) == size); + if (fclose(file) != 0) + ok = false; + free(data); + return ok; +} + +static ikv_node_t *ikv2_parse_binary_file(const char *path) +{ + FILE *file = NULL; + uint8_t magic[4]; + uint8_t kind = 0u; + uint32_t version = 0u; + uint32_t flags = 0u; + uint32_t entry_count = 0u; + char *root_name = NULL; + ikv_node_t *root = NULL; + ikv2_lazy_root_t *lazy_root = NULL; + + if (!path) + return NULL; + + file = fopen(path, "rb"); + if (!file) + return NULL; + + if (fread(magic, 1u, sizeof(magic), file) != sizeof(magic) || + memcmp(magic, "iKv2", sizeof(magic)) != 0 || + !ikv2_file_read_u8(file, &kind) || + kind != (uint8_t)'b' || + !ikv2_file_read_u32le(file, &version) || + version != IKV_V2 || + !ikv2_file_read_u32le(file, &flags) || + (flags & IKV2_BINARY_FLAGS_INDEXED_ROOT) == 0u) + { + fclose(file); + return NULL; + } + + root_name = ikv2_file_read_string(file); + if (!root_name || !ikv2_file_read_varu32(file, &entry_count)) + { + free(root_name); + fclose(file); + return NULL; + } + + root = ikv_create_object(root_name); + free(root_name); + if (!root || !ikv__object_reserve_for_count(root, entry_count)) + { + if (root) + ikv_free(root); + fclose(file); + return NULL; + } + root->value.object.size = entry_count; + + lazy_root = (ikv2_lazy_root_t *)calloc(1u, sizeof(*lazy_root)); + if (!lazy_root) + { + ikv_free(root); + fclose(file); + return NULL; + } + lazy_root->base.destroy = ikv2_lazy_root_destroy; + lazy_root->base.load_object_key = ikv2_lazy_root_load_object_key; + lazy_root->source_kind = IKV2_SOURCE_FILE; + lazy_root->file_path = ikv2_strdup(path); + lazy_root->entry_count = entry_count; + lazy_root->entries = entry_count ? (ikv2_index_entry_t *)calloc(entry_count, sizeof(*lazy_root->entries)) : NULL; + if (!lazy_root->file_path || (entry_count > 0u && !lazy_root->entries)) + { + ikv2_lazy_root_destroy(&lazy_root->base); + ikv_free(root); + fclose(file); + return NULL; + } + + for (uint32_t i = 0; i < entry_count; ++i) + { + lazy_root->entries[i].key = ikv2_file_read_string(file); + if (!lazy_root->entries[i].key) + { + ikv2_lazy_root_destroy(&lazy_root->base); + ikv_free(root); + fclose(file); + return NULL; + } + } + + for (uint32_t i = 0; i < entry_count; ++i) + { + if (!ikv2_file_read_u8(file, &lazy_root->entries[i].type) || + !ikv2_file_read_u32le(file, &lazy_root->entries[i].payload_offset) || + !ikv2_file_read_u32le(file, &lazy_root->entries[i].payload_size)) + { + ikv2_lazy_root_destroy(&lazy_root->base); + ikv_free(root); + fclose(file); + return NULL; + } + } + + fclose(file); + root->lazy_state = &lazy_root->base; + return root; +} + +static ikv_node_t *ikv2_parse_binary_memory(const void *data, size_t size) +{ + uint8_t *buffer = NULL; + + if (!data || size == 0u) + return NULL; + + buffer = (uint8_t *)malloc(size); + if (!buffer) + return NULL; + memcpy(buffer, data, size); + + { + ikv_node_t *root = ikv2_parse_indexed_binary_buffer(buffer, size); + if (!root) + free(buffer); + return root; + } +} + +const ikv_loader_t ikv_loader_v2 = { + IKV_VERSION_2, + ikv2_write_text_file, + ikv2_parse_text_file, + ikv2_parse_text_string, + ikv2_write_binary_file, + ikv2_write_binary_memory, + ikv2_parse_binary_file, + ikv2_parse_binary_memory, +}; diff --git a/src/loaders/ikv2.h b/src/loaders/ikv2.h new file mode 100644 index 0000000..25900a2 --- /dev/null +++ b/src/loaders/ikv2.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../internal/ikv_internal.h" + +extern const ikv_loader_t ikv_loader_v2;