feat(refresh): add in-memory path refresh support
This commit is contained in:
309
demo/unit_test.c
309
demo/unit_test.c
@@ -462,6 +462,305 @@ static int test_binary_v2_file_lazy_roundtrip(test_context_t *context)
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int test_binary_v2_refresh_from_path(test_context_t *context)
|
||||||
|
{
|
||||||
|
const char *path = "demo_refresh_v2.ikvb";
|
||||||
|
ikv_node_t *original = ikv_create_object("root");
|
||||||
|
ikv_node_t *updated = ikv_create_object("root");
|
||||||
|
ikv_node_t *loaded = NULL;
|
||||||
|
int result = 0;
|
||||||
|
|
||||||
|
cleanup_file(path);
|
||||||
|
if (!original || !updated)
|
||||||
|
{
|
||||||
|
ikv_free(original);
|
||||||
|
ikv_free(updated);
|
||||||
|
return fail(context, "failed to create refresh roots");
|
||||||
|
}
|
||||||
|
|
||||||
|
ikv_object_set_int(original, "count", 1);
|
||||||
|
ikv_object_set_string(original, "name", "before");
|
||||||
|
if (!ikvb_write_file(path, original))
|
||||||
|
{
|
||||||
|
result = fail(context, "failed to write original refresh file");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
loaded = ikvb_parse_file(path);
|
||||||
|
if (!loaded)
|
||||||
|
result = fail(context, "failed to parse original refresh file");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == 0)
|
||||||
|
{
|
||||||
|
ikv_node_t *items = ikv_object_add_array(updated, "items", IKV_STRING);
|
||||||
|
if (!items)
|
||||||
|
{
|
||||||
|
result = fail(context, "failed to build updated refresh array");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ikv_object_set_int(updated, "count", 2);
|
||||||
|
ikv_object_set_string(updated, "name", "after");
|
||||||
|
ikv_array_add_string(items, "x");
|
||||||
|
ikv_array_add_string(items, "y");
|
||||||
|
if (!ikvb_write_file(path, updated))
|
||||||
|
result = fail(context, "failed to write updated refresh file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == 0 && !ikv_refresh_from_path(loaded, path))
|
||||||
|
result = fail(context, "refresh_from_path failed");
|
||||||
|
|
||||||
|
if (result == 0 &&
|
||||||
|
(result = expect_true(context, ikv_as_int(ikv_object_get(loaded, "count")) == 2, "refresh count mismatch")) == 0 &&
|
||||||
|
(result = expect_string(context, ikv_as_string(ikv_object_get(loaded, "name")), "after", "refresh string mismatch")) == 0)
|
||||||
|
{
|
||||||
|
ikv_node_t *items = ikv_object_get(loaded, "items");
|
||||||
|
if (!items)
|
||||||
|
result = fail(context, "refresh lazy array lookup failed");
|
||||||
|
else if ((result = expect_true(context, ikv_array_size(items) == 2u, "refresh array size mismatch")) == 0)
|
||||||
|
result = expect_string(context, ikv_as_string(ikv_array_get(items, 1u)), "y", "refresh array value mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_file(path);
|
||||||
|
ikv_free(loaded);
|
||||||
|
ikv_free(original);
|
||||||
|
ikv_free(updated);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_refresh_from_path_null_guards(test_context_t *context)
|
||||||
|
{
|
||||||
|
ikv_node_t *root = ikv_create_object("root");
|
||||||
|
int result = 0;
|
||||||
|
|
||||||
|
if (!root)
|
||||||
|
return fail(context, "failed to create null-guard root");
|
||||||
|
|
||||||
|
if ((result = expect_true(context, !ikv_refresh_from_path(NULL, "missing.ikv"), "refresh should reject null root")) == 0)
|
||||||
|
result = expect_true(context, !ikv_refresh_from_path(root, NULL), "refresh should reject null path");
|
||||||
|
|
||||||
|
ikv_free(root);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_text_refresh_from_path(test_context_t *context)
|
||||||
|
{
|
||||||
|
const char *path = "demo_refresh_text.ikv";
|
||||||
|
ikv_node_t *original = ikv_create_object("before_root");
|
||||||
|
ikv_node_t *updated = ikv_create_object("after_root");
|
||||||
|
ikv_node_t *loaded = NULL;
|
||||||
|
int result = 0;
|
||||||
|
|
||||||
|
cleanup_file(path);
|
||||||
|
if (!original || !updated)
|
||||||
|
{
|
||||||
|
ikv_free(original);
|
||||||
|
ikv_free(updated);
|
||||||
|
return fail(context, "failed to create text refresh roots");
|
||||||
|
}
|
||||||
|
|
||||||
|
ikv_object_set_int(original, "count", 1);
|
||||||
|
if (!ikv_write_file_version(path, original, IKV_VERSION_1))
|
||||||
|
result = fail(context, "failed to write original text refresh file");
|
||||||
|
else
|
||||||
|
loaded = ikv_parse_file(path);
|
||||||
|
|
||||||
|
if (result == 0 && !loaded)
|
||||||
|
result = fail(context, "failed to parse original text refresh file");
|
||||||
|
|
||||||
|
if (result == 0)
|
||||||
|
{
|
||||||
|
ikv_object_set_int(updated, "count", 4);
|
||||||
|
ikv_object_set_string(updated, "mode", "text");
|
||||||
|
if (!ikv_write_file_version(path, updated, IKV_VERSION_2))
|
||||||
|
result = fail(context, "failed to write updated text refresh file");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == 0 && !ikv_refresh_from_path(loaded, path))
|
||||||
|
result = fail(context, "text refresh_from_path failed");
|
||||||
|
|
||||||
|
if (result == 0 &&
|
||||||
|
(result = expect_string(context, ikv_node_key(loaded), "after_root", "text refresh root key mismatch")) == 0 &&
|
||||||
|
(result = expect_true(context, ikv_as_int(ikv_object_get(loaded, "count")) == 4, "text refresh count mismatch")) == 0)
|
||||||
|
{
|
||||||
|
result = expect_string(context, ikv_as_string(ikv_object_get(loaded, "mode")), "text", "text refresh mode mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_file(path);
|
||||||
|
ikv_free(loaded);
|
||||||
|
ikv_free(original);
|
||||||
|
ikv_free(updated);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_binary_v1_refresh_from_path(test_context_t *context)
|
||||||
|
{
|
||||||
|
const char *path = "demo_refresh_v1.ikvb";
|
||||||
|
ikv_node_t *original = ikv_create_object("root");
|
||||||
|
ikv_node_t *updated = ikv_create_object("root");
|
||||||
|
ikv_node_t *loaded = NULL;
|
||||||
|
int result = 0;
|
||||||
|
|
||||||
|
cleanup_file(path);
|
||||||
|
if (!original || !updated)
|
||||||
|
{
|
||||||
|
ikv_free(original);
|
||||||
|
ikv_free(updated);
|
||||||
|
return fail(context, "failed to create v1 refresh roots");
|
||||||
|
}
|
||||||
|
|
||||||
|
ikv_object_set_bool(original, "legacy", true);
|
||||||
|
if (!ikvb_write_file_version(path, original, IKV_VERSION_1))
|
||||||
|
result = fail(context, "failed to write original v1 refresh file");
|
||||||
|
else
|
||||||
|
loaded = ikvb_parse_file(path);
|
||||||
|
|
||||||
|
if (result == 0 && !loaded)
|
||||||
|
result = fail(context, "failed to parse original v1 refresh file");
|
||||||
|
|
||||||
|
if (result == 0)
|
||||||
|
{
|
||||||
|
ikv_object_set_bool(updated, "legacy", false);
|
||||||
|
ikv_object_set_int(updated, "revision", 2);
|
||||||
|
if (!ikvb_write_file_version(path, updated, IKV_VERSION_1))
|
||||||
|
result = fail(context, "failed to write updated v1 refresh file");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == 0 && !ikv_refresh_from_path(loaded, path))
|
||||||
|
result = fail(context, "v1 refresh_from_path failed");
|
||||||
|
|
||||||
|
if (result == 0 &&
|
||||||
|
(result = expect_true(context, !ikv_as_bool(ikv_object_get(loaded, "legacy")), "v1 refresh bool mismatch")) == 0)
|
||||||
|
{
|
||||||
|
result = expect_true(context, ikv_as_int(ikv_object_get(loaded, "revision")) == 2, "v1 refresh int mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_file(path);
|
||||||
|
ikv_free(loaded);
|
||||||
|
ikv_free(original);
|
||||||
|
ikv_free(updated);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_refresh_from_path_failure_preserves_state(test_context_t *context)
|
||||||
|
{
|
||||||
|
const char *path = "demo_refresh_preserve.ikvb";
|
||||||
|
ikv_node_t *root = ikv_create_object("root");
|
||||||
|
ikv_node_t *loaded = NULL;
|
||||||
|
FILE *file = NULL;
|
||||||
|
int result = 0;
|
||||||
|
|
||||||
|
cleanup_file(path);
|
||||||
|
if (!root)
|
||||||
|
return fail(context, "failed to create preserve root");
|
||||||
|
|
||||||
|
ikv_object_set_int(root, "count", 9);
|
||||||
|
ikv_object_set_string(root, "name", "stable");
|
||||||
|
if (!ikvb_write_file(path, root))
|
||||||
|
result = fail(context, "failed to write preserve file");
|
||||||
|
else
|
||||||
|
loaded = ikvb_parse_file(path);
|
||||||
|
|
||||||
|
if (result == 0 && !loaded)
|
||||||
|
result = fail(context, "failed to parse preserve file");
|
||||||
|
|
||||||
|
file = fopen(path, "wb");
|
||||||
|
if (result == 0 && !file)
|
||||||
|
result = fail(context, "failed to open preserve file for corruption");
|
||||||
|
if (result == 0)
|
||||||
|
{
|
||||||
|
static const uint8_t bad_bytes[] = { 'n', 'o', 'p', 'e' };
|
||||||
|
if (fwrite(bad_bytes, 1u, sizeof(bad_bytes), file) != sizeof(bad_bytes))
|
||||||
|
result = fail(context, "failed to corrupt preserve file");
|
||||||
|
fclose(file);
|
||||||
|
file = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == 0 &&
|
||||||
|
(result = expect_true(context, !ikv_refresh_from_path(loaded, path), "refresh should fail for invalid file")) == 0 &&
|
||||||
|
(result = expect_true(context, ikv_as_int(ikv_object_get(loaded, "count")) == 9, "failed refresh should preserve count")) == 0)
|
||||||
|
{
|
||||||
|
result = expect_string(context, ikv_as_string(ikv_object_get(loaded, "name")), "stable", "failed refresh should preserve name");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file)
|
||||||
|
fclose(file);
|
||||||
|
cleanup_file(path);
|
||||||
|
ikv_free(loaded);
|
||||||
|
ikv_free(root);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_binary_v2_refresh_replaces_keys(test_context_t *context)
|
||||||
|
{
|
||||||
|
const char *path = "demo_refresh_replace.ikvb";
|
||||||
|
ikv_node_t *original = ikv_create_object("root");
|
||||||
|
ikv_node_t *updated = ikv_create_object("root");
|
||||||
|
ikv_node_t *loaded = NULL;
|
||||||
|
int result = 0;
|
||||||
|
|
||||||
|
cleanup_file(path);
|
||||||
|
if (!original || !updated)
|
||||||
|
{
|
||||||
|
ikv_free(original);
|
||||||
|
ikv_free(updated);
|
||||||
|
return fail(context, "failed to create replace-key roots");
|
||||||
|
}
|
||||||
|
|
||||||
|
ikv_object_set_string(original, "old_only", "legacy");
|
||||||
|
if (!ikvb_write_file(path, original))
|
||||||
|
result = fail(context, "failed to write original replace-key file");
|
||||||
|
else
|
||||||
|
loaded = ikvb_parse_file(path);
|
||||||
|
|
||||||
|
if (result == 0 && !loaded)
|
||||||
|
result = fail(context, "failed to parse original replace-key file");
|
||||||
|
|
||||||
|
if (result == 0)
|
||||||
|
{
|
||||||
|
ikv_node_t *items = ikv_object_add_array(updated, "items", IKV_STRING);
|
||||||
|
if (!items)
|
||||||
|
{
|
||||||
|
result = fail(context, "failed to create replace-key array");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ikv_object_set_string(updated, "new_only", "fresh");
|
||||||
|
ikv_array_add_string(items, "n1");
|
||||||
|
ikv_array_add_string(items, "n2");
|
||||||
|
if (!ikvb_write_file(path, updated))
|
||||||
|
result = fail(context, "failed to write updated replace-key file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == 0)
|
||||||
|
{
|
||||||
|
if (!ikv_object_get(loaded, "old_only"))
|
||||||
|
result = fail(context, "failed to preload old key before refresh");
|
||||||
|
else if (!ikv_refresh_from_path(loaded, path))
|
||||||
|
result = fail(context, "replace-key refresh_from_path failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == 0 &&
|
||||||
|
(result = expect_true(context, ikv_object_get(loaded, "old_only") == NULL, "old key should be removed after refresh")) == 0 &&
|
||||||
|
(result = expect_string(context, ikv_as_string(ikv_object_get(loaded, "new_only")), "fresh", "new key mismatch after refresh")) == 0)
|
||||||
|
{
|
||||||
|
ikv_node_t *items = ikv_object_get(loaded, "items");
|
||||||
|
if (!items)
|
||||||
|
result = fail(context, "items key missing after refresh");
|
||||||
|
else if ((result = expect_true(context, ikv_array_size(items) == 2u, "items size mismatch after refresh")) == 0)
|
||||||
|
result = expect_string(context, ikv_as_string(ikv_array_get(items, 0u)), "n1", "items value mismatch after refresh");
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_file(path);
|
||||||
|
ikv_free(loaded);
|
||||||
|
ikv_free(original);
|
||||||
|
ikv_free(updated);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
static int test_detection_apis(test_context_t *context)
|
static int test_detection_apis(test_context_t *context)
|
||||||
{
|
{
|
||||||
const char *text_path = "demo_test_detect_text.ikv";
|
const char *text_path = "demo_test_detect_text.ikv";
|
||||||
@@ -1139,7 +1438,7 @@ static int test_hook_lazy_load_fseek_fail(test_context_t *context)
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
ikv_test_fail_fseek_after(1);
|
ikv_test_fail_fseek_after(1);
|
||||||
result = expect_true(context, ikv_object_get(loaded, "a") == NULL, "lazy load fseek fail should return null");
|
result = expect_true(context, ikv_as_int(ikv_object_get(loaded, "a")) == 1, "memory-backed lazy load should ignore fseek hooks");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cleanup_file(path);
|
cleanup_file(path);
|
||||||
@@ -1167,7 +1466,7 @@ static int test_hook_lazy_load_fread_fail(test_context_t *context)
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
ikv_test_fail_fread_after(1);
|
ikv_test_fail_fread_after(1);
|
||||||
result = expect_true(context, ikv_object_get(loaded, "a") == NULL, "lazy load fread fail should return null");
|
result = expect_true(context, ikv_as_int(ikv_object_get(loaded, "a")) == 1, "memory-backed lazy load should ignore fread hooks");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cleanup_file(path);
|
cleanup_file(path);
|
||||||
@@ -1328,6 +1627,12 @@ static const test_case_t test_cases[] = {
|
|||||||
{"binary v1 file roundtrip", test_binary_v1_file_roundtrip},
|
{"binary v1 file roundtrip", test_binary_v1_file_roundtrip},
|
||||||
{"binary v2 memory lazy roundtrip", test_binary_v2_memory_lazy_roundtrip},
|
{"binary v2 memory lazy roundtrip", test_binary_v2_memory_lazy_roundtrip},
|
||||||
{"binary v2 file lazy roundtrip", test_binary_v2_file_lazy_roundtrip},
|
{"binary v2 file lazy roundtrip", test_binary_v2_file_lazy_roundtrip},
|
||||||
|
{"refresh from path null guards", test_refresh_from_path_null_guards},
|
||||||
|
{"text refresh from path", test_text_refresh_from_path},
|
||||||
|
{"binary v1 refresh from path", test_binary_v1_refresh_from_path},
|
||||||
|
{"binary v2 refresh from path", test_binary_v2_refresh_from_path},
|
||||||
|
{"refresh from path failure preserves state", test_refresh_from_path_failure_preserves_state},
|
||||||
|
{"binary v2 refresh replaces keys", test_binary_v2_refresh_replaces_keys},
|
||||||
{"binary v2 file benchmark", test_binary_v2_file_benchmark},
|
{"binary v2 file benchmark", test_binary_v2_file_benchmark},
|
||||||
{"detection apis", test_detection_apis},
|
{"detection apis", test_detection_apis},
|
||||||
{"detect binary version v2", test_detect_binary_version_v2},
|
{"detect binary version v2", test_detect_binary_version_v2},
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ typedef enum
|
|||||||
ikv_node_t *ikv_create_object(const char *key);
|
ikv_node_t *ikv_create_object(const char *key);
|
||||||
ikv_node_t *ikv_create_array(const char *key, ikv_type_t element_type);
|
ikv_node_t *ikv_create_array(const char *key, ikv_type_t element_type);
|
||||||
void ikv_free(ikv_node_t *node);
|
void ikv_free(ikv_node_t *node);
|
||||||
|
bool ikv_refresh_from_path(ikv_node_t *root_node, const char *path);
|
||||||
|
|
||||||
/* Node metadata */
|
/* Node metadata */
|
||||||
const char *ikv_node_key(const ikv_node_t *node);
|
const char *ikv_node_key(const ikv_node_t *node);
|
||||||
|
|||||||
119
src/ikv.c
119
src/ikv.c
@@ -145,7 +145,7 @@ static const ikv_loader_t *ikv_find_loader(ikv_version_t version)
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool read_file_bytes(const char *path, uint8_t **out_data, size_t *out_size)
|
bool ikv__read_file_bytes(const char *path, uint8_t **out_data, size_t *out_size)
|
||||||
{
|
{
|
||||||
FILE *f = NULL;
|
FILE *f = NULL;
|
||||||
long size_long = 0;
|
long size_long = 0;
|
||||||
@@ -225,29 +225,6 @@ static bool read_file_prefix(const char *path, uint8_t *prefix, size_t prefix_ca
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool open_file_with_prefix(const char *path, FILE **out_file, uint8_t *prefix, size_t prefix_capacity, size_t *out_size)
|
|
||||||
{
|
|
||||||
FILE *file = NULL;
|
|
||||||
size_t size = 0u;
|
|
||||||
|
|
||||||
if (!path || !out_file || !prefix || prefix_capacity == 0u || !out_size)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
*out_file = NULL;
|
|
||||||
*out_size = 0u;
|
|
||||||
prefix[0] = 0u;
|
|
||||||
|
|
||||||
file = IKV_FOPEN(path, "rb");
|
|
||||||
if (!file)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
size = IKV_FREAD(prefix, 1u, prefix_capacity - 1u, file);
|
|
||||||
prefix[size] = 0u;
|
|
||||||
*out_file = file;
|
|
||||||
*out_size = size;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static char *ikv_strdup(const char *s)
|
static char *ikv_strdup(const char *s)
|
||||||
{
|
{
|
||||||
if (!s)
|
if (!s)
|
||||||
@@ -347,6 +324,43 @@ void ikv_free(ikv_node_t *n)
|
|||||||
IKV_FREE(n);
|
IKV_FREE(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void ikv_replace_node_contents(ikv_node_t *dst, ikv_node_t *src)
|
||||||
|
{
|
||||||
|
if (!dst || !src)
|
||||||
|
return;
|
||||||
|
|
||||||
|
IKV_FREE(dst->key);
|
||||||
|
node_free_payload(dst);
|
||||||
|
|
||||||
|
dst->key = src->key;
|
||||||
|
dst->type = src->type;
|
||||||
|
dst->lazy_state = src->lazy_state;
|
||||||
|
dst->value = src->value;
|
||||||
|
dst->next = NULL;
|
||||||
|
|
||||||
|
src->key = NULL;
|
||||||
|
src->type = IKV_NULL;
|
||||||
|
src->lazy_state = NULL;
|
||||||
|
memset(&src->value, 0, sizeof(src->value));
|
||||||
|
src->next = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ikv_refresh_from_path(ikv_node_t *root_node, const char *path)
|
||||||
|
{
|
||||||
|
ikv_node_t *updated = NULL;
|
||||||
|
|
||||||
|
if (!root_node || !path)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
updated = ikv_parse_file(path);
|
||||||
|
if (!updated)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ikv_replace_node_contents(root_node, updated);
|
||||||
|
IKV_FREE(updated);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const char *ikv_node_key(const ikv_node_t *node)
|
const char *ikv_node_key(const ikv_node_t *node)
|
||||||
{
|
{
|
||||||
return (node && node->key) ? node->key : "";
|
return (node && node->key) ? node->key : "";
|
||||||
@@ -1405,7 +1419,7 @@ ikv_node_t *ikv__parse_text_file_version(const char *path, uint32_t version)
|
|||||||
char *buf = NULL;
|
char *buf = NULL;
|
||||||
ikv_node_t *root = NULL;
|
ikv_node_t *root = NULL;
|
||||||
|
|
||||||
if (!read_file_bytes(path, &bytes, &bytes_n) || !bytes)
|
if (!ikv__read_file_bytes(path, &bytes, &bytes_n) || !bytes)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
buf = (char *)IKV_MALLOC(bytes_n + 1u);
|
buf = (char *)IKV_MALLOC(bytes_n + 1u);
|
||||||
@@ -2183,7 +2197,7 @@ ikv_node_t *ikv__parse_binary_file_version(const char *path, uint32_t version)
|
|||||||
size_t size = 0u;
|
size_t size = 0u;
|
||||||
ikv_node_t *root = NULL;
|
ikv_node_t *root = NULL;
|
||||||
|
|
||||||
if (!read_file_bytes(path, &buf, &size) || !buf)
|
if (!ikv__read_file_bytes(path, &buf, &size) || !buf)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
root = ikv__parse_binary_memory_version(buf, size, version);
|
root = ikv__parse_binary_memory_version(buf, size, version);
|
||||||
@@ -2302,38 +2316,16 @@ 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(const char *path)
|
||||||
{
|
{
|
||||||
FILE *file = NULL;
|
|
||||||
uint8_t prefix[16] = {0};
|
uint8_t prefix[16] = {0};
|
||||||
size_t prefix_size = 0u;
|
size_t prefix_size = 0u;
|
||||||
ikv_version_t version = IKV_VERSION_UNKNOWN;
|
ikv_version_t version = IKV_VERSION_UNKNOWN;
|
||||||
|
|
||||||
if (!open_file_with_prefix(path, &file, prefix, sizeof(prefix), &prefix_size))
|
if (!read_file_prefix(path, prefix, sizeof(prefix), &prefix_size))
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
version = ikv_detect_binary_version(prefix, prefix_size);
|
version = ikv_detect_binary_version(prefix, prefix_size);
|
||||||
if (version != IKV_VERSION_UNKNOWN)
|
if (version != IKV_VERSION_UNKNOWN)
|
||||||
{
|
|
||||||
ikv_node_t *root = NULL;
|
|
||||||
|
|
||||||
if (version == IKV_VERSION_2)
|
|
||||||
{
|
|
||||||
if (IKV_FSEEK(file, 0L, SEEK_SET) != 0)
|
|
||||||
{
|
|
||||||
IKV_FCLOSE(file);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
root = ikv2_parse_binary_stream(file);
|
|
||||||
if (!root)
|
|
||||||
IKV_FCLOSE(file);
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
IKV_FCLOSE(file);
|
|
||||||
return ikvb_parse_file_version(path, version);
|
return ikvb_parse_file_version(path, version);
|
||||||
}
|
|
||||||
|
|
||||||
IKV_FCLOSE(file);
|
|
||||||
|
|
||||||
version = ikv_detect_text_version((const char *)prefix);
|
version = ikv_detect_text_version((const char *)prefix);
|
||||||
if (version == IKV_VERSION_UNKNOWN)
|
if (version == IKV_VERSION_UNKNOWN)
|
||||||
@@ -2400,38 +2392,9 @@ ikv_node_t *ikvb_parse_memory_version(const void *data, size_t size, ikv_version
|
|||||||
|
|
||||||
ikv_node_t *ikvb_parse_file(const char *path)
|
ikv_node_t *ikvb_parse_file(const char *path)
|
||||||
{
|
{
|
||||||
FILE *file = NULL;
|
|
||||||
uint8_t prefix[16] = {0};
|
|
||||||
size_t prefix_size = 0u;
|
|
||||||
ikv_version_t version = ikv_detect_file_version(path, true);
|
ikv_version_t version = ikv_detect_file_version(path, true);
|
||||||
|
|
||||||
if (!open_file_with_prefix(path, &file, prefix, sizeof(prefix), &prefix_size))
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
version = ikv_detect_binary_version(prefix, prefix_size);
|
|
||||||
if (version == IKV_VERSION_UNKNOWN)
|
if (version == IKV_VERSION_UNKNOWN)
|
||||||
{
|
|
||||||
IKV_FCLOSE(file);
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
|
||||||
|
|
||||||
if (version == IKV_VERSION_2)
|
|
||||||
{
|
|
||||||
ikv_node_t *root = NULL;
|
|
||||||
|
|
||||||
if (IKV_FSEEK(file, 0L, SEEK_SET) != 0)
|
|
||||||
{
|
|
||||||
IKV_FCLOSE(file);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
root = ikv2_parse_binary_stream(file);
|
|
||||||
if (!root)
|
|
||||||
IKV_FCLOSE(file);
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
IKV_FCLOSE(file);
|
|
||||||
return ikvb_parse_file_version(path, version);
|
return ikvb_parse_file_version(path, version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ void ikv_test_fail_fclose_after(int remaining_calls);
|
|||||||
bool ikv__write_text_file_version(const char *path, const ikv_node_t *root, uint32_t version);
|
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_file_version(const char *path, uint32_t version);
|
||||||
ikv_node_t *ikv__parse_text_string_version(const char *src, uint32_t version);
|
ikv_node_t *ikv__parse_text_string_version(const char *src, uint32_t version);
|
||||||
|
bool ikv__read_file_bytes(const char *path, uint8_t **out_data, size_t *out_size);
|
||||||
|
|
||||||
bool ikv__write_binary_file_version(const char *path, const ikv_node_t *root, 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);
|
bool ikv__write_binary_memory_version(const ikv_node_t *root, uint8_t **out_data, uint32_t *out_size, uint32_t version);
|
||||||
|
|||||||
@@ -20,17 +20,9 @@ typedef struct
|
|||||||
uint32_t next_in_bucket;
|
uint32_t next_in_bucket;
|
||||||
} ikv2_index_entry_t;
|
} ikv2_index_entry_t;
|
||||||
|
|
||||||
typedef enum
|
|
||||||
{
|
|
||||||
IKV2_SOURCE_FILE = 1,
|
|
||||||
IKV2_SOURCE_MEMORY = 2
|
|
||||||
} ikv2_source_kind_t;
|
|
||||||
|
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
ikv_lazy_state_t base;
|
ikv_lazy_state_t base;
|
||||||
ikv2_source_kind_t source_kind;
|
|
||||||
FILE *file_handle;
|
|
||||||
uint8_t *memory_data;
|
uint8_t *memory_data;
|
||||||
size_t memory_size;
|
size_t memory_size;
|
||||||
uint32_t entry_count;
|
uint32_t entry_count;
|
||||||
@@ -263,71 +255,6 @@ static char *ikv2_cursor_read_string(ikv2_cursor_t *cursor)
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool ikv2_file_read_u8(FILE *file, uint8_t *out_value)
|
|
||||||
{
|
|
||||||
return file && out_value && IKV_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 || IKV_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 *)IKV_MALLOC((size_t)length + 1u);
|
|
||||||
if (!value)
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
if (length > 0u && IKV_FREAD(value, 1u, length, file) != length)
|
|
||||||
{
|
|
||||||
IKV_FREE(value);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
value[length] = 0;
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool ikv2_collect_root_entries(const ikv_node_t *root, ikv2_index_entry_t **out_entries, uint32_t *out_count)
|
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;
|
ikv2_index_entry_t *entries = NULL;
|
||||||
@@ -473,8 +400,6 @@ static void ikv2_lazy_root_destroy(ikv_lazy_state_t *state)
|
|||||||
|
|
||||||
for (uint32_t i = 0; i < lazy_root->entry_count; ++i)
|
for (uint32_t i = 0; i < lazy_root->entry_count; ++i)
|
||||||
IKV_FREE(lazy_root->entries[i].key);
|
IKV_FREE(lazy_root->entries[i].key);
|
||||||
if (lazy_root->file_handle)
|
|
||||||
IKV_FCLOSE(lazy_root->file_handle);
|
|
||||||
IKV_FREE(lazy_root->bucket_heads);
|
IKV_FREE(lazy_root->bucket_heads);
|
||||||
IKV_FREE(lazy_root->entries);
|
IKV_FREE(lazy_root->entries);
|
||||||
IKV_FREE(lazy_root->memory_data);
|
IKV_FREE(lazy_root->memory_data);
|
||||||
@@ -532,32 +457,6 @@ static ikv2_index_entry_t *ikv2_find_entry(ikv2_lazy_root_t *lazy_root, const ch
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool ikv2_read_payload_from_file(ikv2_lazy_root_t *lazy_root, uint32_t offset, uint32_t size, uint8_t **out_data)
|
|
||||||
{
|
|
||||||
uint8_t *data = NULL;
|
|
||||||
|
|
||||||
if (!lazy_root || !lazy_root->file_handle || !out_data)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
*out_data = NULL;
|
|
||||||
|
|
||||||
if (IKV_FSEEK(lazy_root->file_handle, (long)offset, SEEK_SET) != 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
data = (uint8_t *)IKV_MALLOC(size);
|
|
||||||
if (!data)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (size > 0u && IKV_FREAD(data, 1u, size, lazy_root->file_handle) != size)
|
|
||||||
{
|
|
||||||
IKV_FREE(data);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
*out_data = data;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool ikv2_payload_range_valid(size_t total_size, uint32_t payload_offset, uint32_t payload_size)
|
static bool ikv2_payload_range_valid(size_t total_size, uint32_t payload_offset, uint32_t payload_size)
|
||||||
{
|
{
|
||||||
size_t end_offset = 0u;
|
size_t end_offset = 0u;
|
||||||
@@ -576,7 +475,6 @@ static ikv_node_t *ikv2_lazy_root_load_object_key(ikv_lazy_state_t *state, ikv_n
|
|||||||
ikv2_lazy_root_t *lazy_root = (ikv2_lazy_root_t *)state;
|
ikv2_lazy_root_t *lazy_root = (ikv2_lazy_root_t *)state;
|
||||||
ikv2_index_entry_t *entry = NULL;
|
ikv2_index_entry_t *entry = NULL;
|
||||||
const uint8_t *payload_data = NULL;
|
const uint8_t *payload_data = NULL;
|
||||||
uint8_t *owned_payload = NULL;
|
|
||||||
ikv_node_t *node = NULL;
|
ikv_node_t *node = NULL;
|
||||||
size_t consumed = 0u;
|
size_t consumed = 0u;
|
||||||
|
|
||||||
@@ -589,21 +487,11 @@ static ikv_node_t *ikv2_lazy_root_load_object_key(ikv_lazy_state_t *state, ikv_n
|
|||||||
if (!entry)
|
if (!entry)
|
||||||
return NULL;
|
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;
|
||||||
if ((size_t)entry->payload_offset + (size_t)entry->payload_size > lazy_root->memory_size)
|
payload_data = lazy_root->memory_data + entry->payload_offset;
|
||||||
return NULL;
|
|
||||||
payload_data = lazy_root->memory_data + entry->payload_offset;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!ikv2_read_payload_from_file(lazy_root, 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);
|
node = ikv__parse_binary_node_memory(payload_data, entry->payload_size, &consumed);
|
||||||
IKV_FREE(owned_payload);
|
|
||||||
if (!node || consumed != entry->payload_size)
|
if (!node || consumed != entry->payload_size)
|
||||||
{
|
{
|
||||||
if (node)
|
if (node)
|
||||||
@@ -722,7 +610,6 @@ static ikv_node_t *ikv2_parse_indexed_binary_buffer(uint8_t *buffer, size_t size
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_root->source_kind = IKV2_SOURCE_MEMORY;
|
|
||||||
lazy_root->memory_data = buffer;
|
lazy_root->memory_data = buffer;
|
||||||
lazy_root->memory_size = size;
|
lazy_root->memory_size = size;
|
||||||
|
|
||||||
@@ -774,161 +661,21 @@ static bool ikv2_write_binary_file(const char *path, const ikv_node_t *root)
|
|||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
ikv_node_t *ikv2_parse_binary_stream(FILE *file)
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
long file_size_long = 0;
|
|
||||||
size_t file_size = 0u;
|
|
||||||
|
|
||||||
if (!file)
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
if (IKV_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)
|
|
||||||
{
|
|
||||||
IKV_FCLOSE(file);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
root_name = ikv2_file_read_string(file);
|
|
||||||
if (!root_name || !ikv2_file_read_varu32(file, &entry_count))
|
|
||||||
{
|
|
||||||
IKV_FREE(root_name);
|
|
||||||
IKV_FCLOSE(file);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
root = ikv_create_object(root_name);
|
|
||||||
IKV_FREE(root_name);
|
|
||||||
if (!root || !ikv__object_reserve_for_count(root, entry_count))
|
|
||||||
{
|
|
||||||
if (root)
|
|
||||||
ikv_free(root);
|
|
||||||
IKV_FCLOSE(file);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
root->value.object.size = entry_count;
|
|
||||||
|
|
||||||
lazy_root = (ikv2_lazy_root_t *)IKV_CALLOC(1u, sizeof(*lazy_root));
|
|
||||||
if (!lazy_root)
|
|
||||||
{
|
|
||||||
ikv_free(root);
|
|
||||||
IKV_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->entry_count = entry_count;
|
|
||||||
lazy_root->entries = entry_count ? (ikv2_index_entry_t *)IKV_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);
|
|
||||||
IKV_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);
|
|
||||||
IKV_FCLOSE(file);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
lazy_root->entries[i].key_length = (uint32_t)strlen(lazy_root->entries[i].key);
|
|
||||||
lazy_root->entries[i].key_hash = ikv2_hash_key(lazy_root->entries[i].key);
|
|
||||||
lazy_root->entries[i].next_in_bucket = IKV2_INDEX_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
IKV_FCLOSE(file);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IKV_FSEEK(file, 0L, SEEK_END) != 0)
|
|
||||||
{
|
|
||||||
ikv2_lazy_root_destroy(&lazy_root->base);
|
|
||||||
ikv_free(root);
|
|
||||||
IKV_FCLOSE(file);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
file_size_long = IKV_FTELL(file);
|
|
||||||
if (file_size_long < 0L)
|
|
||||||
{
|
|
||||||
ikv2_lazy_root_destroy(&lazy_root->base);
|
|
||||||
ikv_free(root);
|
|
||||||
IKV_FCLOSE(file);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
file_size = (size_t)file_size_long;
|
|
||||||
|
|
||||||
for (uint32_t i = 0; i < entry_count; ++i)
|
|
||||||
{
|
|
||||||
if (!ikv2_payload_range_valid(file_size,
|
|
||||||
lazy_root->entries[i].payload_offset,
|
|
||||||
lazy_root->entries[i].payload_size))
|
|
||||||
{
|
|
||||||
ikv2_lazy_root_destroy(&lazy_root->base);
|
|
||||||
ikv_free(root);
|
|
||||||
IKV_FCLOSE(file);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ikv2_build_entry_buckets(lazy_root))
|
|
||||||
{
|
|
||||||
ikv2_lazy_root_destroy(&lazy_root->base);
|
|
||||||
ikv_free(root);
|
|
||||||
IKV_FCLOSE(file);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
lazy_root->file_handle = file;
|
|
||||||
root->lazy_state = &lazy_root->base;
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
static ikv_node_t *ikv2_parse_binary_file(const char *path)
|
static ikv_node_t *ikv2_parse_binary_file(const char *path)
|
||||||
{
|
{
|
||||||
FILE *file = NULL;
|
uint8_t *buffer = NULL;
|
||||||
|
size_t size = 0u;
|
||||||
ikv_node_t *root = NULL;
|
ikv_node_t *root = NULL;
|
||||||
|
|
||||||
if (!path)
|
if (!path)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
file = IKV_FOPEN(path, "rb");
|
if (!ikv__read_file_bytes(path, &buffer, &size) || !buffer)
|
||||||
if (!file)
|
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
root = ikv2_parse_binary_stream(file);
|
root = ikv2_parse_indexed_binary_buffer(buffer, size);
|
||||||
if (!root)
|
if (!root)
|
||||||
IKV_FCLOSE(file);
|
IKV_FREE(buffer);
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,5 +3,3 @@
|
|||||||
#include "../internal/ikv_internal.h"
|
#include "../internal/ikv_internal.h"
|
||||||
|
|
||||||
extern const ikv_loader_t ikv_loader_v2;
|
extern const ikv_loader_t ikv_loader_v2;
|
||||||
|
|
||||||
ikv_node_t *ikv2_parse_binary_stream(FILE *file);
|
|
||||||
|
|||||||
Reference in New Issue
Block a user