summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-06-29 21:43:54 +0900
committertanaka takashi <isazaka@gmail.com>2026-06-30 01:23:51 +0900
commit4bc721c3bf7589b519c5730d093f704ce56e3f09 (patch)
tree38da055ab15fd70f5c92584560ea73486e5ddce0 /src
parent4c6a831aa2a57e2bcc20aa3fc2f1b7172f5be285 (diff)
refactor: MCP層をmcpc APIベースに刷新
- 独自JSON-RPC処理をmcpcへ置き換え - 各MCPツールを個別ソースへ分離 - 共通コンテキストと登録処理を整理 - mcpc互換修正と統合テストを更新
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt27
-rw-r--r--src/app/main.c43
-rw-r--r--src/base/allocator.c107
-rw-r--r--src/base/allocator.h23
-rw-r--r--src/domain/message.c49
-rw-r--r--src/domain/search.c53
-rw-r--r--src/mcp/README.md22
-rw-r--r--src/mcp/json.c61
-rw-r--r--src/mcp/server.c762
-rw-r--r--src/mcp/tools/common/common.c117
-rw-r--r--src/mcp/tools/common/common.h19
-rw-r--r--src/mcp/tools/common/context.c88
-rw-r--r--src/mcp/tools/common/context.h20
-rw-r--r--src/mcp/tools/common/registry.c11
-rw-r--r--src/mcp/tools/common/registry.h13
-rw-r--r--src/mcp/tools/common/tools.h10
-rw-r--r--src/mcp/tools/delete_message.c50
-rw-r--r--src/mcp/tools/get_system_health.c49
-rw-r--r--src/mcp/tools/read_messages.c80
-rw-r--r--src/mcp/tools/rebuild_index.c57
-rw-r--r--src/mcp/tools/search_messages.c134
-rw-r--r--src/mcp/tools/write_message.c213
-rw-r--r--src/storage/README.md3
-rw-r--r--src/storage/codec.c76
-rw-r--r--src/storage/codec.h13
-rw-r--r--src/storage/store.c647
-rw-r--r--src/storage/store.h3
-rw-r--r--src/sys/README.md21
-rw-r--r--src/sys/encoding.c208
-rw-r--r--src/sys/encoding.h24
-rw-r--r--src/sys/system.c890
-rw-r--r--src/sys/system.h90
32 files changed, 2844 insertions, 1139 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index e7768d7..e441c55 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,19 +1,44 @@
+add_library(tiny-convmem-base STATIC base/allocator.c)
+target_include_directories(tiny-convmem-base PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}")
+target_compile_features(tiny-convmem-base PUBLIC c_std_17)
+
+add_library(tiny-convmem-sys STATIC sys/system.c sys/encoding.c)
+target_include_directories(tiny-convmem-sys PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}")
+target_compile_features(tiny-convmem-sys PUBLIC c_std_17)
+target_link_libraries(tiny-convmem-sys PUBLIC tiny-convmem-base)
+
+if(WIN32)
+ target_link_libraries(tiny-convmem-sys PRIVATE bcrypt shell32)
+endif()
+
add_executable(tiny-convmem-mcp
app/main.c
domain/message.c
domain/search.c
+ storage/codec.c
storage/store.c
mcp/json.c
mcp/server.c
+ mcp/tools/common/common.c
+ mcp/tools/common/context.c
+ mcp/tools/common/registry.c
+ mcp/tools/delete_message.c
+ mcp/tools/get_system_health.c
+ mcp/tools/read_messages.c
+ mcp/tools/rebuild_index.c
+ mcp/tools/search_messages.c
+ mcp/tools/write_message.c
)
target_include_directories(tiny-convmem-mcp PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}"
"${PROJECT_SOURCE_DIR}/third_party/mcpc/src"
)
-target_link_libraries(tiny-convmem-mcp PRIVATE mcpc::mcpc bcrypt)
+target_link_libraries(tiny-convmem-mcp PRIVATE mcpc::mcpc tiny-convmem-sys)
target_compile_features(tiny-convmem-mcp PRIVATE c_std_17)
if(MSVC)
+ target_compile_options(tiny-convmem-base PRIVATE /utf-8 /W4)
+ target_compile_options(tiny-convmem-sys PRIVATE /utf-8 /W4)
target_compile_options(tiny-convmem-mcp PRIVATE /utf-8 /W4)
endif()
diff --git a/src/app/main.c b/src/app/main.c
index ce43b61..f73b314 100644
--- a/src/app/main.c
+++ b/src/app/main.c
@@ -1,43 +1,41 @@
-#define WIN32_LEAN_AND_MEAN
-#include <windows.h>
-
#include <stdio.h>
#include <stdlib.h>
-#include <wchar.h>
+#include <string.h>
#include "mcp/server.h"
#include "storage/store.h"
+#include "sys/system.h"
+
+#include "base/allocator.h"
static void
usage(FILE *stream)
{
- fwprintf(stream, L"Usage: tiny-convmem-mcp --data-dir <path>\n");
+ fprintf(stream, "Usage: tiny-convmem-mcp --data-dir <path>\n");
}
-int
-wmain(int argc, wchar_t **argv)
+static int
+run(const tiny_sys_arguments_t *arguments)
{
- wchar_t *absolute = NULL;
- DWORD required;
+ char *absolute;
tiny_store_t *store = NULL;
tiny_store_status_t status;
int result;
- if (argc == 2 && wcscmp(argv[1], L"--help") == 0)
+ if (arguments->count == 2 && strcmp(arguments->values[1], "--help") == 0)
{
usage(stdout);
return 0;
}
- if (argc != 3 || wcscmp(argv[1], L"--data-dir") != 0 || argv[2][0] == L'\0')
+ if (arguments->count != 3 || strcmp(arguments->values[1], "--data-dir") != 0
+ || arguments->values[2][0] == '\0')
{
usage(stderr);
return 2;
}
- required = GetFullPathNameW(argv[2], 0, NULL, NULL);
- if (required == 0 || (absolute = malloc((size_t)required * sizeof(wchar_t))) == NULL
- || GetFullPathNameW(argv[2], required, absolute, NULL) == 0)
+ absolute = tiny_sys_path_absolute(arguments->values[2]);
+ if (absolute == NULL)
{
fprintf(stderr, "unable to resolve --data-dir\n");
- free(absolute);
return 2;
}
status = tiny_store_open(&store, absolute);
@@ -55,3 +53,18 @@ wmain(int argc, wchar_t **argv)
}
return result;
}
+
+int
+main(int argc, char **argv)
+{
+ tiny_sys_arguments_t arguments;
+ int result;
+ if (!tiny_sys_arguments_init(&arguments, argc, argv))
+ {
+ fprintf(stderr, "unable to read process arguments\n");
+ return 2;
+ }
+ result = run(&arguments);
+ tiny_sys_arguments_free(&arguments);
+ return result;
+}
diff --git a/src/base/allocator.c b/src/base/allocator.c
new file mode 100644
index 0000000..3a5c515
--- /dev/null
+++ b/src/base/allocator.c
@@ -0,0 +1,107 @@
+#define TINY_ALLOCATOR_IMPLEMENTATION
+#include "base/allocator.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+
+static size_t fail_index = SIZE_MAX;
+static size_t calls;
+static size_t outstanding;
+
+static int
+should_fail(void)
+{
+ return calls++ == fail_index;
+}
+
+void *
+tiny_alloc(size_t size)
+{
+ void *result;
+ if (should_fail())
+ {
+ return NULL;
+ }
+ result = malloc(size);
+ if (result != NULL)
+ {
+ outstanding++;
+ }
+ return result;
+}
+
+void *
+tiny_calloc(size_t count, size_t size)
+{
+ void *result;
+ if ((size != 0 && count > SIZE_MAX / size) || should_fail())
+ {
+ return NULL;
+ }
+ result = calloc(count, size);
+ if (result != NULL)
+ {
+ outstanding++;
+ }
+ return result;
+}
+
+void *
+tiny_realloc(void *memory, size_t size)
+{
+ void *result;
+ if (size == 0)
+ {
+ tiny_free(memory);
+ return NULL;
+ }
+ if (should_fail())
+ {
+ return NULL;
+ }
+ result = realloc(memory, size);
+ if (result != NULL && memory == NULL)
+ {
+ outstanding++;
+ }
+ return result;
+}
+
+void
+tiny_free(void *memory)
+{
+ if (memory != NULL)
+ {
+ free(memory);
+ if (outstanding > 0)
+ {
+ outstanding--;
+ }
+ }
+}
+
+void
+tiny_allocator_reset(void)
+{
+ fail_index = SIZE_MAX;
+ calls = 0;
+}
+
+void
+tiny_allocator_fail_after(size_t allocation_index)
+{
+ fail_index = allocation_index;
+ calls = 0;
+}
+
+size_t
+tiny_allocator_call_count(void)
+{
+ return calls;
+}
+
+size_t
+tiny_allocator_outstanding(void)
+{
+ return outstanding;
+}
diff --git a/src/base/allocator.h b/src/base/allocator.h
new file mode 100644
index 0000000..626e294
--- /dev/null
+++ b/src/base/allocator.h
@@ -0,0 +1,23 @@
+#ifndef TINY_CONVMEM_BASE_ALLOCATOR_H
+#define TINY_CONVMEM_BASE_ALLOCATOR_H
+
+#include <stddef.h>
+
+void *tiny_alloc(size_t size);
+void *tiny_calloc(size_t count, size_t size);
+void *tiny_realloc(void *memory, size_t size);
+void tiny_free(void *memory);
+
+void tiny_allocator_reset(void);
+void tiny_allocator_fail_after(size_t allocation_index);
+size_t tiny_allocator_call_count(void);
+size_t tiny_allocator_outstanding(void);
+
+#ifndef TINY_ALLOCATOR_IMPLEMENTATION
+#define malloc tiny_alloc
+#define calloc tiny_calloc
+#define realloc tiny_realloc
+#define free tiny_free
+#endif
+
+#endif
diff --git a/src/domain/message.c b/src/domain/message.c
index d973be4..b1db3e1 100644
--- a/src/domain/message.c
+++ b/src/domain/message.c
@@ -3,6 +3,8 @@
#include <stdlib.h>
#include <string.h>
+#include "base/allocator.h"
+
static char *
duplicate_string(const char *value)
{
@@ -15,9 +17,20 @@ duplicate_string(const char *value)
return copy;
}
+static int
+copy_failed(tiny_message_t *message)
+{
+ tiny_message_free(message);
+ return -1;
+}
+
void
tiny_message_init(tiny_message_t *message)
{
+ if (message == NULL)
+ {
+ return;
+ }
memset(message, 0, sizeof(*message));
message->metadata.memory_tier = TINY_TIER_SHORT;
}
@@ -26,6 +39,10 @@ void
tiny_message_free(tiny_message_t *message)
{
size_t index;
+ if (message == NULL)
+ {
+ return;
+ }
free(message->session_id);
free(message->content);
free(message->metadata.tool_call_id);
@@ -41,6 +58,12 @@ int
tiny_message_copy(tiny_message_t *destination, const tiny_message_t *source)
{
size_t index;
+ if (destination == NULL || source == NULL || destination == source || source->session_id == NULL
+ || source->content == NULL
+ || (source->metadata.tag_count != 0 && source->metadata.tags == NULL))
+ {
+ return -1;
+ }
tiny_message_init(destination);
memcpy(destination->message_id, source->message_id, sizeof(destination->message_id));
destination->timestamp_ms = source->timestamp_ms;
@@ -50,14 +73,14 @@ tiny_message_copy(tiny_message_t *destination, const tiny_message_t *source)
destination->content = duplicate_string(source->content);
if (destination->session_id == NULL || destination->content == NULL)
{
- goto failure;
+ return copy_failed(destination);
}
if (source->metadata.tool_call_id != NULL)
{
destination->metadata.tool_call_id = duplicate_string(source->metadata.tool_call_id);
if (destination->metadata.tool_call_id == NULL)
{
- goto failure;
+ return copy_failed(destination);
}
}
if (source->metadata.tag_count > 0)
@@ -65,23 +88,19 @@ tiny_message_copy(tiny_message_t *destination, const tiny_message_t *source)
destination->metadata.tags = calloc(source->metadata.tag_count, sizeof(char *));
if (destination->metadata.tags == NULL)
{
- goto failure;
+ return copy_failed(destination);
}
for (index = 0; index < source->metadata.tag_count; ++index)
{
destination->metadata.tags[index] = duplicate_string(source->metadata.tags[index]);
if (destination->metadata.tags[index] == NULL)
{
- goto failure;
+ return copy_failed(destination);
}
destination->metadata.tag_count++;
}
}
return 0;
-
-failure:
- tiny_message_free(destination);
- return -1;
}
const char *
@@ -103,6 +122,10 @@ tiny_role_name(tiny_role_t role)
bool
tiny_role_parse(const char *value, tiny_role_t *role)
{
+ if (value == NULL || role == NULL)
+ {
+ return false;
+ }
if (strcmp(value, "user") == 0)
{
*role = TINY_ROLE_USER;
@@ -143,6 +166,10 @@ tiny_tier_name(tiny_memory_tier_t tier)
bool
tiny_tier_parse(const char *value, tiny_memory_tier_t *tier)
{
+ if (value == NULL || tier == NULL)
+ {
+ return false;
+ }
if (strcmp(value, "short") == 0)
{
*tier = TINY_TIER_SHORT;
@@ -170,6 +197,10 @@ bool
tiny_utf8_valid(const char *value, size_t length)
{
size_t index = 0;
+ if (value == NULL)
+ {
+ return length == 0;
+ }
while (index < length)
{
const unsigned char first = (unsigned char)value[index++];
@@ -240,7 +271,7 @@ tiny_message_id_valid(const char *value)
{
static const char alphabet[] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
size_t index;
- if (strlen(value) != TINY_MESSAGE_ID_LENGTH || value[0] > '7')
+ if (value == NULL || strlen(value) != TINY_MESSAGE_ID_LENGTH || value[0] > '7')
{
return false;
}
diff --git a/src/domain/search.c b/src/domain/search.c
index a24b667..b0ac440 100644
--- a/src/domain/search.c
+++ b/src/domain/search.c
@@ -4,6 +4,8 @@
#include <stdlib.h>
#include <string.h>
+#include "base/allocator.h"
+
typedef struct gram_bucket
{
uint64_t hash;
@@ -25,9 +27,25 @@ struct tiny_search_index
static bool
decode_codepoints(const char *text, uint32_t **output, size_t *count)
{
- const size_t bytes = strlen(text);
+ size_t bytes;
size_t offset = 0, used = 0;
- uint32_t *points = malloc((bytes + 1) * sizeof(uint32_t));
+ uint32_t *points;
+ if (output == NULL || count == NULL)
+ {
+ return false;
+ }
+ *output = NULL;
+ *count = 0;
+ if (text == NULL)
+ {
+ return false;
+ }
+ bytes = strlen(text);
+ if (!tiny_utf8_valid(text, bytes) || bytes > SIZE_MAX / sizeof(uint32_t) - 1)
+ {
+ return false;
+ }
+ points = malloc((bytes + 1) * sizeof(uint32_t));
if (points == NULL)
{
return false;
@@ -198,6 +216,12 @@ tiny_search_index_add(tiny_search_index_t *index,
size_t message_index)
{
size_t tag;
+ if (index == NULL || message == NULL || message->content == NULL
+ || (message->metadata.tag_count != 0 && message->metadata.tags == NULL)
+ || message_index == SIZE_MAX)
+ {
+ return false;
+ }
if (!index_text(index, message->content, message_index))
{
return false;
@@ -226,8 +250,16 @@ tiny_search_index_candidates(const tiny_search_index_t *index,
size_t point_count = 0, width, offset, used = 0;
bool *seen;
size_t *items;
+ if (output == NULL || count == NULL)
+ {
+ return false;
+ }
*output = NULL;
*count = 0;
+ if (index == NULL || query == NULL || index->message_count > SIZE_MAX / sizeof(*items))
+ {
+ return false;
+ }
if (!decode_codepoints(query, &points, &point_count))
{
return false;
@@ -283,10 +315,20 @@ tiny_search_score(const char *query, const tiny_message_t *message)
size_t query_count = 0, message_count = 0, width, query_offset, message_offset, matches = 0,
total, tag;
double score = 0.0;
- if (!decode_codepoints(query, &query_points, &query_count)
- || !decode_codepoints(message->content, &message_points, &message_count) || query_count == 0)
+ if (query == NULL || message == NULL || message->content == NULL
+ || (message->metadata.tag_count != 0 && message->metadata.tags == NULL))
+ {
+ return 0.0;
+ }
+ if (!decode_codepoints(query, &query_points, &query_count) || query_count == 0)
+ {
+ free(query_points);
+ return 0.0;
+ }
+ if (!decode_codepoints(message->content, &message_points, &message_count))
{
- goto done;
+ free(query_points);
+ return 0.0;
}
width = query_count >= 3 ? 3 : (query_count >= 2 ? 2 : 1);
total = query_count - width + 1;
@@ -343,7 +385,6 @@ tiny_search_score(const char *query, const tiny_message_t *message)
}
}
}
-done:
free(query_points);
free(message_points);
return score;
diff --git a/src/mcp/README.md b/src/mcp/README.md
index dc7ac46..ed008a0 100644
--- a/src/mcp/README.md
+++ b/src/mcp/README.md
@@ -1,14 +1,14 @@
-# MCP compatibility boundary
+# MCP adapter
-The pinned mcpc revision cannot describe required versus optional properties,
-nested objects/arrays, 64-bit values, or application callback context. The
-project therefore keeps mcpc immutable and implements the stdio dispatch and
-tool schemas in `server.c`, using mcpc's bundled `mjson` parser for raw JSON.
+`server.c` owns only the mcpc server lifecycle. Tool schemas and callbacks live
+under `tools/`, one source file per memory tool. Shared registration, callback
+context, and response helpers live under `tools/common/`. mcpc owns stdio
+framing, MCP initialization, dispatch, and result envelopes.
-This boundary supports the MCP 2024-11-05 initialize, tools/list, and tools/call
-flow needed by the server. Tool success and application errors are returned as
-JSON serialized into TextContent; application errors also set `isError`.
+The pinned mcpc API supports integer and UTF-8 string properties, but not nested
+or optional properties. Consequently every advertised property must be sent.
+Use an empty `session_id` where that filter is optional, and pass write metadata
+as a JSON object string in `metadata_json` (`{}` when no metadata is needed).
-If mcpc is upgraded, this boundary should be removed only after its integration
-tests demonstrate equivalent optional/object/array schema and callback-state
-behavior.
+Tool success and application errors are JSON serialized into MCP TextContent;
+application errors also set `isError`.
diff --git a/src/mcp/json.c b/src/mcp/json.c
index dcef91b..62f31c9 100644
--- a/src/mcp/json.c
+++ b/src/mcp/json.c
@@ -4,11 +4,17 @@
#include <stdlib.h>
#include <string.h>
+#include "base/allocator.h"
+
static bool
reserve(tiny_json_buffer_t *buffer, size_t addition)
{
size_t needed, capacity;
char *resized;
+ if (buffer == NULL)
+ {
+ return false;
+ }
if (buffer->failed || addition > SIZE_MAX - buffer->length - 1)
{
return false;
@@ -42,11 +48,18 @@ reserve(tiny_json_buffer_t *buffer, size_t addition)
void
tiny_json_init(tiny_json_buffer_t *buffer)
{
- memset(buffer, 0, sizeof(*buffer));
+ if (buffer != NULL)
+ {
+ memset(buffer, 0, sizeof(*buffer));
+ }
}
void
tiny_json_free(tiny_json_buffer_t *buffer)
{
+ if (buffer == NULL)
+ {
+ return;
+ }
free(buffer->data);
tiny_json_init(buffer);
}
@@ -54,7 +67,7 @@ tiny_json_free(tiny_json_buffer_t *buffer)
bool
tiny_json_raw_n(tiny_json_buffer_t *buffer, const char *value, size_t length)
{
- if (!reserve(buffer, length))
+ if ((value == NULL && length != 0) || !reserve(buffer, length))
{
return false;
}
@@ -66,14 +79,14 @@ tiny_json_raw_n(tiny_json_buffer_t *buffer, const char *value, size_t length)
bool
tiny_json_raw(tiny_json_buffer_t *buffer, const char *value)
{
- return tiny_json_raw_n(buffer, value, strlen(value));
+ return value != NULL && tiny_json_raw_n(buffer, value, strlen(value));
}
bool
tiny_json_string(tiny_json_buffer_t *buffer, const char *value)
{
const unsigned char *position = (const unsigned char *)value;
- if (!tiny_json_raw(buffer, "\""))
+ if (value == NULL || !tiny_json_raw(buffer, "\""))
{
return false;
}
@@ -85,36 +98,60 @@ tiny_json_string(tiny_json_buffer_t *buffer, const char *value)
escaped[0] = '\\';
escaped[1] = (char)*position;
escaped[2] = '\0';
- tiny_json_raw(buffer, escaped);
+ if (!tiny_json_raw(buffer, escaped))
+ {
+ return false;
+ }
}
else if (*position == '\b')
{
- tiny_json_raw(buffer, "\\b");
+ if (!tiny_json_raw(buffer, "\\b"))
+ {
+ return false;
+ }
}
else if (*position == '\f')
{
- tiny_json_raw(buffer, "\\f");
+ if (!tiny_json_raw(buffer, "\\f"))
+ {
+ return false;
+ }
}
else if (*position == '\n')
{
- tiny_json_raw(buffer, "\\n");
+ if (!tiny_json_raw(buffer, "\\n"))
+ {
+ return false;
+ }
}
else if (*position == '\r')
{
- tiny_json_raw(buffer, "\\r");
+ if (!tiny_json_raw(buffer, "\\r"))
+ {
+ return false;
+ }
}
else if (*position == '\t')
{
- tiny_json_raw(buffer, "\\t");
+ if (!tiny_json_raw(buffer, "\\t"))
+ {
+ return false;
+ }
}
else if (*position < 0x20)
{
snprintf(escaped, sizeof(escaped), "\\u%04x", *position);
- tiny_json_raw(buffer, escaped);
+ if (!tiny_json_raw(buffer, escaped))
+ {
+ return false;
+ }
}
else
{
- tiny_json_raw_n(buffer, (const char *)position, 1);
+ if (!tiny_json_raw_n(buffer, (const char *)position, 1))
+ {
+ return false;
+ }
}
++position;
}
diff --git a/src/mcp/server.c b/src/mcp/server.c
index 80d8307..a0ff436 100644
--- a/src/mcp/server.c
+++ b/src/mcp/server.c
@@ -1,757 +1,37 @@
#include "mcp/server.h"
-#include "domain/search.h"
-#include "mcp/json.h"
-#include "mjson.h"
+#include "mcp/tools/common/tools.h"
-#include <math.h>
-#include <stdint.h>
-#include <stdlib.h>
-#include <string.h>
-
-#define MCP_PROTOCOL_VERSION "2024-11-05"
-
-typedef struct result_item
-{
- const tiny_store_entry_t *entry;
- double score;
-} result_item_t;
-
-static char *
-read_line(FILE *input)
-{
- size_t length = 0, capacity = 4096;
- int character;
- char *line = malloc(capacity);
- if (!line)
- {
- return NULL;
- }
- while ((character = fgetc(input)) != EOF)
- {
- if (length + 1 >= capacity)
- {
- char *r;
- capacity *= 2;
- r = realloc(line, capacity);
- if (!r)
- {
- free(line);
- return NULL;
- }
- line = r;
- }
- line[length++] = (char)character;
- if (character == '\n')
- {
- break;
- }
- }
- if (length == 0 && character == EOF)
- {
- free(line);
- return NULL;
- }
- line[length] = '\0';
- return line;
-}
-
-static char *
-json_string_value(
- const char *json, int length, const char *path, size_t maximum, bool required, bool *valid)
-{
- const char *token = NULL;
- int token_length = 0;
- int type = mjson_find(json, length, path, &token, &token_length);
- char *value;
- int decoded;
- if (type == MJSON_TOK_INVALID)
- {
- if (required)
- {
- *valid = false;
- }
- return NULL;
- }
- if (type != MJSON_TOK_STRING || token_length < 2 || (size_t)token_length > maximum * 6 + 2)
- {
- *valid = false;
- return NULL;
- }
- value = malloc(maximum + 1);
- if (!value)
- {
- *valid = false;
- return NULL;
- }
- decoded = mjson_get_string(json, length, path, value, (int)maximum + 1);
- if (decoded < 0 || (size_t)decoded > maximum || !tiny_utf8_valid(value, (size_t)decoded))
- {
- free(value);
- *valid = false;
- return NULL;
- }
- value[decoded] = '\0';
- return value;
-}
-
-static bool
-object_has_only(const char *json, int length, const char *const *allowed, size_t allowed_count)
-{
- int offset = 0, koff, klen, voff, vlen, vtype;
- while ((offset = mjson_next(json, length, offset, &koff, &klen, &voff, &vlen, &vtype)) != 0)
- {
- char key[64];
- int decoded;
- size_t index;
- if (klen <= 0)
- {
- return false;
- }
- decoded = mjson_get_string(json + koff, klen, "$", key, sizeof(key));
- if (decoded <= 0 || decoded >= (int)sizeof(key))
- {
- return false;
- }
- key[decoded] = '\0';
- for (index = 0; index < allowed_count; ++index)
- {
- if (strcmp(key, allowed[index]) == 0)
- {
- break;
- }
- }
- if (index == allowed_count)
- {
- return false;
- }
- }
- return true;
-}
-
-static void
-free_input_message(tiny_message_t *message)
-{
- tiny_message_free(message);
-}
-
-static bool
-parse_tags(const char *json, int length, tiny_message_t *message)
-{
- const char *array = NULL;
- int array_length = 0;
- int type = mjson_find(json, length, "$.metadata.tags", &array, &array_length);
- int offset = 0, koff, klen, voff, vlen, vtype;
- if (type == MJSON_TOK_INVALID)
- {
- return true;
- }
- if (type != MJSON_TOK_ARRAY)
- {
- return false;
- }
- while ((offset = mjson_next(array, array_length, offset, &koff, &klen, &voff, &vlen, &vtype))
- != 0)
- {
- char *tag;
- int decoded;
- char **resized;
- if (vtype != MJSON_TOK_STRING || message->metadata.tag_count >= TINY_TAG_COUNT_MAX)
- {
- return false;
- }
- tag = malloc(TINY_TAG_MAX + 1);
- if (!tag)
- {
- return false;
- }
- decoded = mjson_get_string(array + voff, vlen, "$", tag, TINY_TAG_MAX + 1);
- if (decoded <= 0 || decoded > TINY_TAG_MAX || !tiny_utf8_valid(tag, (size_t)decoded))
- {
- free(tag);
- return false;
- }
- tag[decoded] = '\0';
- resized = realloc(message->metadata.tags, (message->metadata.tag_count + 1) * sizeof(char *));
- if (!resized)
- {
- free(tag);
- return false;
- }
- message->metadata.tags = resized;
- message->metadata.tags[message->metadata.tag_count++] = tag;
- }
- return true;
-}
-
-static bool
-parse_write(const char *args, int length, tiny_message_t *message)
-{
- static const char *const top_keys[] = { "session_id", "role", "content", "metadata" };
- static const char *const metadata_keys[] = { "tool_call_id", "tags", "memory_tier" };
- bool valid = true;
- char *role = NULL, *tier = NULL;
- const char *metadata = NULL;
- int metadata_length = 0, metadata_type;
- tiny_message_init(message);
- if (!object_has_only(args, length, top_keys, sizeof(top_keys) / sizeof(top_keys[0])))
- {
- valid = false;
- }
- metadata_type = mjson_find(args, length, "$.metadata", &metadata, &metadata_length);
- if (metadata_type != MJSON_TOK_INVALID
- && (metadata_type != MJSON_TOK_OBJECT
- || !object_has_only(metadata,
- metadata_length,
- metadata_keys,
- sizeof(metadata_keys) / sizeof(metadata_keys[0]))))
- {
- valid = false;
- }
- message->session_id
- = json_string_value(args, length, "$.session_id", TINY_SESSION_ID_MAX, true, &valid);
- role = json_string_value(args, length, "$.role", 16, true, &valid);
- message->content = json_string_value(args, length, "$.content", TINY_CONTENT_MAX, true, &valid);
- message->metadata.tool_call_id = json_string_value(
- args, length, "$.metadata.tool_call_id", TINY_TOOL_CALL_ID_MAX, false, &valid);
- tier = json_string_value(args, length, "$.metadata.memory_tier", 16, false, &valid);
- if (!valid || !message->session_id || !message->content || !role || message->session_id[0] == '\0'
- || message->content[0] == '\0' || !tiny_role_parse(role, &message->role)
- || (tier && !tiny_tier_parse(tier, &message->metadata.memory_tier))
- || !parse_tags(args, length, message))
- {
- valid = false;
- }
- free(role);
- free(tier);
- if (!valid)
- {
- free_input_message(message);
- }
- return valid;
-}
-
-static void
-append_message(tiny_json_buffer_t *json, const tiny_message_t *message)
-{
- size_t index;
- tiny_json_raw(json, "{\"message_id\":");
- tiny_json_string(json, message->message_id);
- tiny_json_raw(json, ",\"session_id\":");
- tiny_json_string(json, message->session_id);
- tiny_json_raw(json, ",\"timestamp\":");
- tiny_json_i64(json, message->timestamp_ms);
- tiny_json_raw(json, ",\"role\":");
- tiny_json_string(json, tiny_role_name(message->role));
- tiny_json_raw(json, ",\"content\":");
- tiny_json_string(json, message->content);
- tiny_json_raw(json, ",\"metadata\":{");
- if (message->metadata.tool_call_id)
- {
- tiny_json_raw(json, "\"tool_call_id\":");
- tiny_json_string(json, message->metadata.tool_call_id);
- tiny_json_raw(json, ",");
- }
- tiny_json_raw(json, "\"tags\":[");
- for (index = 0; index < message->metadata.tag_count; ++index)
- {
- if (index)
- {
- tiny_json_raw(json, ",");
- }
- tiny_json_string(json, message->metadata.tags[index]);
- }
- tiny_json_raw(json, "],\"memory_tier\":");
- tiny_json_string(json, tiny_tier_name(message->metadata.memory_tier));
- tiny_json_raw(json, "}}");
-}
-
-static void
-send_json_result(
- FILE *output, const char *id, int id_length, tiny_json_buffer_t *inner, bool is_error)
-{
- tiny_json_buffer_t response;
- tiny_json_init(&response);
- tiny_json_raw(&response, "{\"jsonrpc\":\"2.0\",\"id\":");
- tiny_json_raw_n(&response, id, (size_t)id_length);
- tiny_json_raw(&response, ",\"result\":{\"content\":[{\"type\":\"text\",\"text\":");
- tiny_json_string(&response, inner->data ? inner->data : "{}");
- tiny_json_raw(&response, "}]");
- if (is_error)
- {
- tiny_json_raw(&response, ",\"isError\":true");
- }
- tiny_json_raw(&response, "}}\n");
- if (!response.failed)
- {
- fwrite(response.data, 1, response.length, output);
- fflush(output);
- }
- tiny_json_free(&response);
-}
-
-static void
-send_error(FILE *output, const char *id, int id_length, const char *code, const char *message)
-{
- tiny_json_buffer_t inner;
- tiny_json_init(&inner);
- tiny_json_raw(&inner, "{\"status\":\"ERROR\",\"error\":{\"code\":");
- tiny_json_string(&inner, code);
- tiny_json_raw(&inner, ",\"message\":");
- tiny_json_string(&inner, message);
- tiny_json_raw(&inner, "}}");
- send_json_result(output, id, id_length, &inner, true);
- tiny_json_free(&inner);
-}
-
-static int
-compare_history(const void *left, const void *right)
-{
- const tiny_store_entry_t *a = *(const tiny_store_entry_t *const *)left,
- *b = *(const tiny_store_entry_t *const *)right;
- if (a->message.timestamp_ms < b->message.timestamp_ms)
- {
- return -1;
- }
- if (a->message.timestamp_ms > b->message.timestamp_ms)
- {
- return 1;
- }
- return strcmp(a->message.message_id, b->message.message_id);
-}
-
-static int
-compare_results(const void *l, const void *r)
+int
+tiny_convmem_server_run(FILE *input, FILE *output, tiny_store_t *store)
{
- const result_item_t *a = l, *b = r;
- if (a->score > b->score)
- {
- return -1;
- }
- if (a->score < b->score)
+ tiny_mcp_tools_context_t *context;
+ mcpc_server_t *server;
+ mcpc_errcode_t status;
+ if (!input || !output || !store)
{
return 1;
}
- if (a->entry->message.timestamp_ms > b->entry->message.timestamp_ms)
- {
- return -1;
- }
- if (a->entry->message.timestamp_ms < b->entry->message.timestamp_ms)
+ context = tiny_mcp_tools_context_new(store);
+ if (!context)
{
return 1;
}
- return strcmp(a->entry->message.message_id, b->entry->message.message_id);
-}
-
-static tiny_search_index_t *
-build_search_index(const tiny_store_t *store)
-{
- tiny_search_index_t *index = tiny_search_index_create();
- size_t position;
- if (!index)
- {
- return NULL;
- }
- for (position = 0; position < tiny_store_entry_count(store); ++position)
+ server = mcpc_server_new_iostrm(input, output);
+ if (!server || mcpc_server_set_nament(server, (const char8_t *)"tiny-convmem-mcp") != MCPC_EC_0
+ || mcpc_server_capa_enable_tool(server) != MCPC_EC_0 || !tiny_mcp_tools_register(server)
+ || !tiny_mcp_tools_context_activate(context))
{
- if (!tiny_search_index_add(index, &tiny_store_entry(store, position)->message, position))
- {
- tiny_search_index_free(index);
- return NULL;
- }
- }
- return index;
-}
-
-static void
-handle_call(FILE *output,
- const char *id,
- int id_length,
- const char *name,
- const char *args,
- int args_length,
- tiny_store_t *store,
- tiny_search_index_t **search_index)
-{
- tiny_json_buffer_t result;
- tiny_store_status_t status;
- tiny_json_init(&result);
- if (strcmp(name, "write_message") == 0)
- {
- tiny_message_t input, written;
- if (!parse_write(args, args_length, &input))
- {
- send_error(output, id, id_length, "VALIDATION_ERROR", "invalid write_message arguments");
- return;
- }
- tiny_message_init(&written);
- status = tiny_store_write(store, &input, &written);
- tiny_message_free(&input);
- if (status != TINY_STORE_OK)
- {
- send_error(
- output, id, id_length, tiny_store_status_name(status), "message was not written");
- return;
- }
- if (*search_index
- && !tiny_search_index_add(*search_index, &written, tiny_store_entry_count(store) - 1))
- {
- tiny_search_index_free(*search_index);
- *search_index = NULL;
- }
- tiny_json_raw(&result, "{\"status\":\"OK\",\"message\":");
- append_message(&result, &written);
- tiny_json_raw(&result, "}");
- tiny_message_free(&written);
- }
- else if (strcmp(name, "read_messages") == 0)
- {
- static const char *const keys[] = { "session_id" };
- bool valid = object_has_only(args, args_length, keys, 1);
- char *session
- = json_string_value(args, args_length, "$.session_id", TINY_SESSION_ID_MAX, true, &valid);
- size_t i, count = 0;
- const tiny_store_entry_t **items;
- if (!valid || !session || !session[0])
- {
- free(session);
- send_error(output, id, id_length, "VALIDATION_ERROR", "session_id is required");
- return;
- }
- items = malloc(tiny_store_entry_count(store) * sizeof(*items));
- if (!items)
- {
- free(session);
- send_error(output, id, id_length, "INTERNAL_ERROR", "out of memory");
- return;
- }
- for (i = 0; i < tiny_store_entry_count(store); ++i)
- {
- const tiny_store_entry_t *e = tiny_store_entry(store, i);
- if (!e->deleted && strcmp(e->message.session_id, session) == 0)
- {
- items[count++] = e;
- }
- }
- qsort(items, count, sizeof(*items), compare_history);
- tiny_json_raw(&result, "{\"history\":[");
- for (i = 0; i < count; ++i)
- {
- if (i)
- {
- tiny_json_raw(&result, ",");
- }
- append_message(&result, &items[i]->message);
- }
- tiny_json_raw(&result, "]}");
- free(items);
- free(session);
- }
- else if (strcmp(name, "search_messages") == 0)
- {
- static const char *const keys[] = { "query", "session_id", "limit" };
- bool valid = object_has_only(args, args_length, keys, 3);
- char *query = json_string_value(args, args_length, "$.query", TINY_QUERY_MAX, true, &valid);
- char *session = json_string_value(
- args, args_length, "$.session_id", TINY_SESSION_ID_MAX, false, &valid);
- double number = 20;
- size_t limit = 20, i, count = 0, candidate_count = 0, *candidates = NULL;
- result_item_t *items;
- if (mjson_find(args, args_length, "$.limit", NULL, NULL) != MJSON_TOK_INVALID)
- {
- if (!mjson_get_number(args, args_length, "$.limit", &number) || number < 1 || number > 100
- || floor(number) != number)
- {
- valid = false;
- }
- else
- {
- limit = (size_t)number;
- }
- }
- if (!valid || !query || !query[0])
- {
- free(query);
- free(session);
- send_error(output, id, id_length, "VALIDATION_ERROR", "query or limit is invalid");
- return;
- }
- if (!*search_index)
+ if (server)
{
- *search_index = build_search_index(store);
+ mcpc_server_close(server);
}
- if (!*search_index
- || !tiny_search_index_candidates(*search_index, query, &candidates, &candidate_count))
- {
- free(query);
- free(session);
- send_error(output, id, id_length, "INTERNAL_ERROR", "search index allocation failed");
- return;
- }
- items = malloc((candidate_count ? candidate_count : 1) * sizeof(*items));
- if (!items)
- {
- free(candidates);
- free(query);
- free(session);
- send_error(output, id, id_length, "INTERNAL_ERROR", "out of memory");
- return;
- }
- for (i = 0; i < candidate_count; ++i)
- {
- const tiny_store_entry_t *e = tiny_store_entry(store, candidates[i]);
- double score;
- if (!e || e->deleted || (session && strcmp(e->message.session_id, session) != 0))
- {
- continue;
- }
- score = tiny_search_score(query, &e->message);
- if (score > 0)
- {
- items[count].entry = e;
- items[count].score = score;
- count++;
- }
- }
- qsort(items, count, sizeof(*items), compare_results);
- if (count > limit)
- {
- count = limit;
- }
- tiny_json_raw(&result, "{\"results\":[");
- for (i = 0; i < count; ++i)
- {
- if (i)
- {
- tiny_json_raw(&result, ",");
- }
- append_message(&result, &items[i].entry->message);
- }
- tiny_json_raw(&result, "]}");
- free(candidates);
- free(items);
- free(query);
- free(session);
- }
- else if (strcmp(name, "delete_message") == 0)
- {
- static const char *const keys[] = { "message_id", "session_id" };
- bool valid = object_has_only(args, args_length, keys, 2);
- char *mid = json_string_value(
- args, args_length, "$.message_id", TINY_MESSAGE_ID_LENGTH, true, &valid);
- char *session = json_string_value(
- args, args_length, "$.session_id", TINY_SESSION_ID_MAX, false, &valid);
- if (!valid || !mid || !tiny_message_id_valid(mid))
- {
- free(mid);
- free(session);
- send_error(output,
- id,
- id_length,
- "VALIDATION_ERROR",
- "message_id must be a canonical 26-character ULID");
- return;
- }
- status = tiny_store_delete(store, mid, session);
- free(mid);
- free(session);
- if (status != TINY_STORE_OK && status != TINY_STORE_NOT_FOUND)
- {
- send_error(output,
- id,
- id_length,
- tiny_store_status_name(status),
- "delete record was not written");
- return;
- }
- tiny_json_raw(&result,
- status == TINY_STORE_OK ? "{\"status\":\"OK\"}" : "{\"status\":\"NOT_FOUND\"}");
- }
- else if (strcmp(name, "rebuild_index") == 0)
- {
- tiny_rebuild_report_t report;
- tiny_search_index_t *rebuilt;
- if (!object_has_only(args, args_length, NULL, 0))
- {
- send_error(output, id, id_length, "VALIDATION_ERROR", "rebuild_index takes no arguments");
- return;
- }
- status = tiny_store_rebuild(store, &report);
- if (status != TINY_STORE_OK)
- {
- send_error(output, id, id_length, tiny_store_status_name(status), "index rebuild failed");
- return;
- }
- rebuilt = build_search_index(store);
- if (!rebuilt)
- {
- tiny_search_index_free(*search_index);
- *search_index = NULL;
- send_error(output,
- id,
- id_length,
- "INTERNAL_ERROR",
- "log was compacted but the search index could not be allocated");
- return;
- }
- tiny_search_index_free(*search_index);
- *search_index = rebuilt;
- tiny_json_raw(&result, "{\"status\":\"OK\",\"messages_indexed\":");
- tiny_json_u64(&result, report.messages_indexed);
- tiny_json_raw(&result, ",\"records_removed\":");
- tiny_json_u64(&result, report.records_removed);
- tiny_json_raw(&result, ",\"bytes_before\":");
- tiny_json_u64(&result, report.bytes_before);
- tiny_json_raw(&result, ",\"bytes_after\":");
- tiny_json_u64(&result, report.bytes_after);
- tiny_json_raw(&result, "}");
- }
- else if (strcmp(name, "get_system_health") == 0)
- {
- tiny_store_health_t health;
- if (!object_has_only(args, args_length, NULL, 0))
- {
- send_error(
- output, id, id_length, "VALIDATION_ERROR", "get_system_health takes no arguments");
- return;
- }
- tiny_store_health(store, &health);
- tiny_json_raw(&result, "{\"status\":");
- tiny_json_string(&result, health.last_error[0] ? "DEGRADED" : "OK");
- tiny_json_raw(&result, ",\"format_version\":1,\"log_size\":");
- tiny_json_u64(&result, health.log_size);
- tiny_json_raw(&result, ",\"snapshot_size\":");
- tiny_json_u64(&result, health.snapshot_size);
- tiny_json_raw(&result, ",\"active_messages\":");
- tiny_json_u64(&result, health.active_messages);
- tiny_json_raw(&result, ",\"deleted_messages\":");
- tiny_json_u64(&result, health.deleted_messages);
- tiny_json_raw(&result, ",\"last_sequence\":");
- tiny_json_u64(&result, health.last_sequence);
- tiny_json_raw(&result, ",\"recovered_tail\":");
- tiny_json_raw(&result, health.recovered_tail ? "true" : "false");
- tiny_json_raw(&result, ",\"snapshot_valid\":");
- tiny_json_raw(&result, health.snapshot_valid ? "true" : "false");
- tiny_json_raw(&result, ",\"last_error\":");
- tiny_json_string(&result, health.last_error);
- tiny_json_raw(&result, "}");
- }
- else
- {
- send_error(output, id, id_length, "VALIDATION_ERROR", "unknown tool");
- return;
- }
- if (result.failed)
- {
- send_error(output, id, id_length, "INTERNAL_ERROR", "response allocation failed");
- }
- else
- {
- send_json_result(output, id, id_length, &result, false);
- }
- tiny_json_free(&result);
-}
-
-static const char *tools_json
- = "{\"tools\":["
- "{\"name\":\"write_message\",\"description\":\"Durably append a conversation "
- "message\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"session_id\":{\"type\":"
- "\"string\",\"maxLength\":256},\"role\":{\"type\":\"string\",\"enum\":[\"user\","
- "\"assistant\",\"system\"]},\"content\":{\"type\":\"string\",\"maxLength\":1048576},"
- "\"metadata\":{\"type\":\"object\",\"properties\":{\"tool_call_id\":{\"type\":\"string\","
- "\"maxLength\":256},\"tags\":{\"type\":\"array\",\"maxItems\":32,\"items\":{\"type\":"
- "\"string\",\"maxLength\":128}},\"memory_tier\":{\"type\":\"string\",\"enum\":[\"short\","
- "\"mid\",\"long\",\"discard\"]}},\"additionalProperties\":false}},\"required\":[\"session_"
- "id\",\"role\",\"content\"],\"additionalProperties\":false}},"
- "{\"name\":\"read_messages\",\"description\":\"Read a session in chronological "
- "order\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"session_id\":{\"type\":"
- "\"string\",\"maxLength\":256}},\"required\":[\"session_id\"],\"additionalProperties\":false}"
- "},"
- "{\"name\":\"search_messages\",\"description\":\"Search message content and "
- "tags\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\","
- "\"maxLength\":4096},\"session_id\":{\"type\":\"string\",\"maxLength\":256},\"limit\":{"
- "\"type\":\"integer\",\"minimum\":1,\"maximum\":100}},\"required\":[\"query\"],"
- "\"additionalProperties\":false}},"
- "{\"name\":\"delete_message\",\"description\":\"Durably tombstone a "
- "message\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"message_id\":{\"type\":"
- "\"string\",\"minLength\":26,\"maxLength\":26},\"session_id\":{\"type\":\"string\","
- "\"maxLength\":256}},\"required\":[\"message_id\"],\"additionalProperties\":false}},"
- "{\"name\":\"rebuild_index\",\"description\":\"Compact the log and rebuild the "
- "snapshot\",\"inputSchema\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":"
- "false}},"
- "{\"name\":\"get_system_health\",\"description\":\"Report persistence health and "
- "counters\",\"inputSchema\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":"
- "false}}]}";
-
-int
-tiny_convmem_server_run(FILE *input, FILE *output, tiny_store_t *store)
-{
- char *line;
- tiny_search_index_t *search_index = build_search_index(store);
- if (!search_index)
- {
+ tiny_mcp_tools_context_free(context);
return 1;
}
- while ((line = read_line(input)) != NULL)
- {
- int length = (int)strlen(line);
- const char *method = NULL, *id = NULL, *params = NULL;
- int ml = 0, il = 0, pl = 0;
- if (mjson_find(line, length, "$.method", &method, &ml) != MJSON_TOK_STRING)
- {
- free(line);
- continue;
- }
- mjson_find(line, length, "$.id", &id, &il);
- mjson_find(line, length, "$.params", &params, &pl);
- {
- char method_value[64];
- int decoded
- = mjson_get_string(line, length, "$.method", method_value, sizeof(method_value));
- if (decoded < 0)
- {
- free(line);
- continue;
- }
- method_value[decoded] = '\0';
- if (strcmp(method_value, "initialize") == 0 && id)
- {
- fprintf(output,
- "{\"jsonrpc\":\"2.0\",\"id\":%.*s,\"result\":{\"protocolVersion\":\"%s\","
- "\"capabilities\":{\"tools\":{}},\"serverInfo\":{\"name\":\"tiny-convmem-mcp\","
- "\"version\":\"0.1.0\"}}}\n",
- il,
- id,
- MCP_PROTOCOL_VERSION);
- }
- else if (strcmp(method_value, "tools/list") == 0 && id)
- {
- fprintf(
- output, "{\"jsonrpc\":\"2.0\",\"id\":%.*s,\"result\":%s}\n", il, id, tools_json);
- }
- else if (strcmp(method_value, "tools/call") == 0 && id && params)
- {
- char name[64];
- const char *args = NULL;
- int al = 0;
- int n = mjson_get_string(params, pl, "$.name", name, sizeof(name));
- if (n > 0)
- {
- name[n] = '\0';
- if (mjson_find(params, pl, "$.arguments", &args, &al) == MJSON_TOK_OBJECT)
- {
- handle_call(output, id, il, name, args, al, store, &search_index);
- }
- else
- {
- send_error(output, id, il, "VALIDATION_ERROR", "arguments must be an object");
- }
- }
- }
- fflush(output);
- }
- free(line);
- }
- tiny_search_index_free(search_index);
- return 0;
+ status = mcpc_server_start(server);
+ tiny_mcp_tools_context_deactivate(context);
+ mcpc_server_close(server);
+ tiny_mcp_tools_context_free(context);
+ return status == MCPC_EC_0 ? 0 : 1;
}
diff --git a/src/mcp/tools/common/common.c b/src/mcp/tools/common/common.c
new file mode 100644
index 0000000..71b6f68
--- /dev/null
+++ b/src/mcp/tools/common/common.c
@@ -0,0 +1,117 @@
+#include "mcp/tools/common/common.h"
+
+#include <stdlib.h>
+
+#include "base/allocator.h"
+
+void
+tiny_mcp_tool_add_error(mcpc_ucbr_t *result, const char *code, const char *message)
+{
+ tiny_json_buffer_t json;
+ tiny_json_init(&json);
+ tiny_json_raw(&json, "{\"status\":\"ERROR\",\"error\":{\"code\":");
+ tiny_json_string(&json, code);
+ tiny_json_raw(&json, ",\"message\":");
+ tiny_json_string(&json, message);
+ tiny_json_raw(&json, "}}");
+ mcpc_toolcall_result_add_errmsg_printf8(
+ result, (const char8_t *)"%s", json.data ? json.data : "{}");
+ tiny_json_free(&json);
+}
+
+void
+tiny_mcp_tool_add_json(mcpc_ucbr_t *result, tiny_json_buffer_t *json)
+{
+ if (json->failed)
+ {
+ tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "response allocation failed");
+ return;
+ }
+ mcpc_toolcall_result_add_text_printf8(
+ result, (const char8_t *)"%s", json->data ? json->data : "{}");
+}
+
+char *
+tiny_mcp_tool_get_string(const mcpc_tool_t *tool, const char *name, size_t maximum)
+{
+ char *value = malloc(maximum + 1);
+ size_t length = 0;
+ if (!value
+ || mcpc_tool_get_tpropval_u8str(
+ tool, (const char8_t *)name, (char8_t *)value, maximum + 1, &length)
+ != MCPC_EC_0
+ || length > maximum)
+ {
+ free(value);
+ return NULL;
+ }
+ value[length] = '\0';
+ if (!tiny_utf8_valid(value, length))
+ {
+ free(value);
+ return NULL;
+ }
+ return value;
+}
+
+void
+tiny_mcp_tool_append_message(tiny_json_buffer_t *json, const tiny_message_t *message)
+{
+ size_t index;
+ tiny_json_raw(json, "{\"message_id\":");
+ tiny_json_string(json, message->message_id);
+ tiny_json_raw(json, ",\"session_id\":");
+ tiny_json_string(json, message->session_id);
+ tiny_json_raw(json, ",\"timestamp\":");
+ tiny_json_i64(json, message->timestamp_ms);
+ tiny_json_raw(json, ",\"role\":");
+ tiny_json_string(json, tiny_role_name(message->role));
+ tiny_json_raw(json, ",\"content\":");
+ tiny_json_string(json, message->content);
+ tiny_json_raw(json, ",\"metadata\":{");
+ if (message->metadata.tool_call_id)
+ {
+ tiny_json_raw(json, "\"tool_call_id\":");
+ tiny_json_string(json, message->metadata.tool_call_id);
+ tiny_json_raw(json, ",");
+ }
+ tiny_json_raw(json, "\"tags\":[");
+ for (index = 0; index < message->metadata.tag_count; ++index)
+ {
+ if (index)
+ {
+ tiny_json_raw(json, ",");
+ }
+ tiny_json_string(json, message->metadata.tags[index]);
+ }
+ tiny_json_raw(json, "],\"memory_tier\":");
+ tiny_json_string(json, tiny_tier_name(message->metadata.memory_tier));
+ tiny_json_raw(json, "}}");
+}
+
+mcpc_tool_t *
+tiny_mcp_tool_new(const char *name, const char *description, mcpc_tcallcb_t callback)
+{
+ mcpc_tool_t *tool = mcpc_tool_new2((const char8_t *)name, (const char8_t *)description);
+ if (!tool || mcpc_tool_set_call_cb(tool, callback) != MCPC_EC_0)
+ {
+ return NULL;
+ }
+ return tool;
+}
+
+bool
+tiny_mcp_tool_add_property(mcpc_tool_t *tool,
+ const char *name,
+ const char *description,
+ mcpc_anytype_t type)
+{
+ mcpc_toolprop_t *property
+ = mcpc_toolprop_new2((const char8_t *)name, (const char8_t *)description, type);
+ if (!property)
+ {
+ return false;
+ }
+ mcpc_tool_add_toolprop(tool, property);
+ return true;
+}
diff --git a/src/mcp/tools/common/common.h b/src/mcp/tools/common/common.h
new file mode 100644
index 0000000..6b1cc96
--- /dev/null
+++ b/src/mcp/tools/common/common.h
@@ -0,0 +1,19 @@
+#ifndef TINY_CONVMEM_MCP_TOOLS_COMMON_H
+#define TINY_CONVMEM_MCP_TOOLS_COMMON_H
+
+#include "domain/message.h"
+#include "mcp/json.h"
+#include <mcpc/mcpc.h>
+
+void tiny_mcp_tool_add_error(mcpc_ucbr_t *result, const char *code, const char *message);
+void tiny_mcp_tool_add_json(mcpc_ucbr_t *result, tiny_json_buffer_t *json);
+char *tiny_mcp_tool_get_string(const mcpc_tool_t *tool, const char *name, size_t maximum);
+void tiny_mcp_tool_append_message(tiny_json_buffer_t *json, const tiny_message_t *message);
+
+mcpc_tool_t *tiny_mcp_tool_new(const char *name, const char *description, mcpc_tcallcb_t callback);
+bool tiny_mcp_tool_add_property(mcpc_tool_t *tool,
+ const char *name,
+ const char *description,
+ mcpc_anytype_t type);
+
+#endif
diff --git a/src/mcp/tools/common/context.c b/src/mcp/tools/common/context.c
new file mode 100644
index 0000000..1a469e0
--- /dev/null
+++ b/src/mcp/tools/common/context.c
@@ -0,0 +1,88 @@
+#include "mcp/tools/common/context.h"
+
+#include <stdlib.h>
+
+#include "base/allocator.h"
+
+/* mcpc's callback API has no user-data parameter. The stdio server is single-instance. */
+static tiny_mcp_tools_context_t *active_context;
+
+tiny_search_index_t *
+tiny_mcp_tools_build_search_index(const tiny_store_t *store)
+{
+ tiny_search_index_t *index = tiny_search_index_create();
+ size_t position;
+ if (!index)
+ {
+ return NULL;
+ }
+ for (position = 0; position < tiny_store_entry_count(store); ++position)
+ {
+ if (!tiny_search_index_add(index, &tiny_store_entry(store, position)->message, position))
+ {
+ tiny_search_index_free(index);
+ return NULL;
+ }
+ }
+ return index;
+}
+
+tiny_mcp_tools_context_t *
+tiny_mcp_tools_context_new(tiny_store_t *store)
+{
+ tiny_mcp_tools_context_t *context;
+ if (!store)
+ {
+ return NULL;
+ }
+ context = malloc(sizeof(*context));
+ if (!context)
+ {
+ return NULL;
+ }
+ context->store = store;
+ context->search_index = tiny_mcp_tools_build_search_index(store);
+ if (!context->search_index)
+ {
+ free(context);
+ return NULL;
+ }
+ return context;
+}
+
+void
+tiny_mcp_tools_context_free(tiny_mcp_tools_context_t *context)
+{
+ if (!context)
+ {
+ return;
+ }
+ tiny_search_index_free(context->search_index);
+ free(context);
+}
+
+bool
+tiny_mcp_tools_context_activate(tiny_mcp_tools_context_t *context)
+{
+ if (!context || active_context)
+ {
+ return false;
+ }
+ active_context = context;
+ return true;
+}
+
+void
+tiny_mcp_tools_context_deactivate(tiny_mcp_tools_context_t *context)
+{
+ if (active_context == context)
+ {
+ active_context = NULL;
+ }
+}
+
+tiny_mcp_tools_context_t *
+tiny_mcp_tools_context_current(void)
+{
+ return active_context;
+}
diff --git a/src/mcp/tools/common/context.h b/src/mcp/tools/common/context.h
new file mode 100644
index 0000000..0847d4b
--- /dev/null
+++ b/src/mcp/tools/common/context.h
@@ -0,0 +1,20 @@
+#ifndef TINY_CONVMEM_MCP_TOOLS_CONTEXT_H
+#define TINY_CONVMEM_MCP_TOOLS_CONTEXT_H
+
+#include "domain/search.h"
+#include "storage/store.h"
+
+typedef struct tiny_mcp_tools_context
+{
+ tiny_store_t *store;
+ tiny_search_index_t *search_index;
+} tiny_mcp_tools_context_t;
+
+tiny_mcp_tools_context_t *tiny_mcp_tools_context_new(tiny_store_t *store);
+void tiny_mcp_tools_context_free(tiny_mcp_tools_context_t *context);
+bool tiny_mcp_tools_context_activate(tiny_mcp_tools_context_t *context);
+void tiny_mcp_tools_context_deactivate(tiny_mcp_tools_context_t *context);
+tiny_mcp_tools_context_t *tiny_mcp_tools_context_current(void);
+tiny_search_index_t *tiny_mcp_tools_build_search_index(const tiny_store_t *store);
+
+#endif
diff --git a/src/mcp/tools/common/registry.c b/src/mcp/tools/common/registry.c
new file mode 100644
index 0000000..9aa1dff
--- /dev/null
+++ b/src/mcp/tools/common/registry.c
@@ -0,0 +1,11 @@
+#include "mcp/tools/common/tools.h"
+
+#include "mcp/tools/common/registry.h"
+
+bool
+tiny_mcp_tools_register(mcpc_server_t *server)
+{
+ return tiny_mcp_register_write_message(server) && tiny_mcp_register_read_messages(server)
+ && tiny_mcp_register_search_messages(server) && tiny_mcp_register_delete_message(server)
+ && tiny_mcp_register_rebuild_index(server) && tiny_mcp_register_get_system_health(server);
+}
diff --git a/src/mcp/tools/common/registry.h b/src/mcp/tools/common/registry.h
new file mode 100644
index 0000000..bbc68d0
--- /dev/null
+++ b/src/mcp/tools/common/registry.h
@@ -0,0 +1,13 @@
+#ifndef TINY_CONVMEM_MCP_TOOLS_REGISTRY_H
+#define TINY_CONVMEM_MCP_TOOLS_REGISTRY_H
+
+#include <mcpc/mcpc.h>
+
+bool tiny_mcp_register_write_message(mcpc_server_t *server);
+bool tiny_mcp_register_read_messages(mcpc_server_t *server);
+bool tiny_mcp_register_search_messages(mcpc_server_t *server);
+bool tiny_mcp_register_delete_message(mcpc_server_t *server);
+bool tiny_mcp_register_rebuild_index(mcpc_server_t *server);
+bool tiny_mcp_register_get_system_health(mcpc_server_t *server);
+
+#endif
diff --git a/src/mcp/tools/common/tools.h b/src/mcp/tools/common/tools.h
new file mode 100644
index 0000000..b06321c
--- /dev/null
+++ b/src/mcp/tools/common/tools.h
@@ -0,0 +1,10 @@
+#ifndef TINY_CONVMEM_MCP_TOOLS_H
+#define TINY_CONVMEM_MCP_TOOLS_H
+
+#include "mcp/tools/common/context.h"
+#define MCPC_C23PTCH_UCHAR2 1
+#include <mcpc/mcpc.h>
+
+bool tiny_mcp_tools_register(mcpc_server_t *server);
+
+#endif
diff --git a/src/mcp/tools/delete_message.c b/src/mcp/tools/delete_message.c
new file mode 100644
index 0000000..ae924d7
--- /dev/null
+++ b/src/mcp/tools/delete_message.c
@@ -0,0 +1,50 @@
+#include "mcp/tools/common/registry.h"
+
+#include "mcp/tools/common/common.h"
+#include "mcp/tools/common/context.h"
+
+#include <stdlib.h>
+
+#include "base/allocator.h"
+
+static void
+call_delete_message(const mcpc_tool_t *tool, mcpc_ucbr_t *result)
+{
+ tiny_mcp_tools_context_t *context = tiny_mcp_tools_context_current();
+ char *message_id = tiny_mcp_tool_get_string(tool, "message_id", TINY_MESSAGE_ID_LENGTH);
+ char *session = tiny_mcp_tool_get_string(tool, "session_id", TINY_SESSION_ID_MAX);
+ tiny_store_status_t status;
+ if (!context || !message_id || !session || !tiny_message_id_valid(message_id))
+ {
+ tiny_mcp_tool_add_error(
+ result, "VALIDATION_ERROR", "message_id must be a canonical 26-character ULID");
+ }
+ else
+ {
+ status = tiny_store_delete(context->store, message_id, session[0] ? session : NULL);
+ if (status != TINY_STORE_OK && status != TINY_STORE_NOT_FOUND)
+ {
+ tiny_mcp_tool_add_error(
+ result, tiny_store_status_name(status), "delete record was not written");
+ }
+ else
+ {
+ mcpc_toolcall_result_add_text_printf8(
+ result,
+ (const char8_t *)(status == TINY_STORE_OK ? "{\"status\":\"OK\"}"
+ : "{\"status\":\"NOT_FOUND\"}"));
+ }
+ }
+ free(message_id);
+ free(session);
+}
+
+bool
+tiny_mcp_register_delete_message(mcpc_server_t *server)
+{
+ mcpc_tool_t *tool = tiny_mcp_tool_new("delete_message", "Delete a message", call_delete_message);
+ return tool && tiny_mcp_tool_add_property(tool, "message_id", "Message ULID", MCPC_U8STR)
+ && tiny_mcp_tool_add_property(
+ tool, "session_id", "Session ID; empty if omitted", MCPC_U8STR)
+ && mcpc_server_add_tool(server, tool) == MCPC_EC_0;
+}
diff --git a/src/mcp/tools/get_system_health.c b/src/mcp/tools/get_system_health.c
new file mode 100644
index 0000000..eb02090
--- /dev/null
+++ b/src/mcp/tools/get_system_health.c
@@ -0,0 +1,49 @@
+#include "mcp/tools/common/registry.h"
+
+#include "mcp/tools/common/common.h"
+#include "mcp/tools/common/context.h"
+
+static void
+call_get_system_health(const mcpc_tool_t *tool, mcpc_ucbr_t *result)
+{
+ tiny_mcp_tools_context_t *context = tiny_mcp_tools_context_current();
+ tiny_store_health_t health;
+ tiny_json_buffer_t json;
+ (void)tool;
+ if (!context)
+ {
+ tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "tool context is unavailable");
+ return;
+ }
+ tiny_store_health(context->store, &health);
+ tiny_json_init(&json);
+ tiny_json_raw(&json, "{\"status\":");
+ tiny_json_string(&json, health.last_error[0] ? "DEGRADED" : "OK");
+ tiny_json_raw(&json, ",\"format_version\":1,\"log_size\":");
+ tiny_json_u64(&json, health.log_size);
+ tiny_json_raw(&json, ",\"snapshot_size\":");
+ tiny_json_u64(&json, health.snapshot_size);
+ tiny_json_raw(&json, ",\"active_messages\":");
+ tiny_json_u64(&json, health.active_messages);
+ tiny_json_raw(&json, ",\"deleted_messages\":");
+ tiny_json_u64(&json, health.deleted_messages);
+ tiny_json_raw(&json, ",\"last_sequence\":");
+ tiny_json_u64(&json, health.last_sequence);
+ tiny_json_raw(&json, ",\"recovered_tail\":");
+ tiny_json_raw(&json, health.recovered_tail ? "true" : "false");
+ tiny_json_raw(&json, ",\"snapshot_valid\":");
+ tiny_json_raw(&json, health.snapshot_valid ? "true" : "false");
+ tiny_json_raw(&json, ",\"last_error\":");
+ tiny_json_string(&json, health.last_error);
+ tiny_json_raw(&json, "}");
+ tiny_mcp_tool_add_json(result, &json);
+ tiny_json_free(&json);
+}
+
+bool
+tiny_mcp_register_get_system_health(mcpc_server_t *server)
+{
+ mcpc_tool_t *tool
+ = tiny_mcp_tool_new("get_system_health", "Report storage health", call_get_system_health);
+ return tool && mcpc_server_add_tool(server, tool) == MCPC_EC_0;
+}
diff --git a/src/mcp/tools/read_messages.c b/src/mcp/tools/read_messages.c
new file mode 100644
index 0000000..55696d6
--- /dev/null
+++ b/src/mcp/tools/read_messages.c
@@ -0,0 +1,80 @@
+#include "mcp/tools/common/registry.h"
+
+#include "mcp/tools/common/common.h"
+#include "mcp/tools/common/context.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "base/allocator.h"
+
+static int
+compare_history(const void *left, const void *right)
+{
+ const tiny_store_entry_t *a = *(const tiny_store_entry_t *const *)left;
+ const tiny_store_entry_t *b = *(const tiny_store_entry_t *const *)right;
+ if (a->message.timestamp_ms < b->message.timestamp_ms)
+ {
+ return -1;
+ }
+ if (a->message.timestamp_ms > b->message.timestamp_ms)
+ {
+ return 1;
+ }
+ return strcmp(a->message.message_id, b->message.message_id);
+}
+
+static void
+call_read_messages(const mcpc_tool_t *tool, mcpc_ucbr_t *result)
+{
+ tiny_mcp_tools_context_t *context = tiny_mcp_tools_context_current();
+ char *session = tiny_mcp_tool_get_string(tool, "session_id", TINY_SESSION_ID_MAX);
+ const tiny_store_entry_t **items = NULL;
+ size_t index, count = 0;
+ tiny_json_buffer_t json;
+ if (!context || !session || !session[0])
+ {
+ free(session);
+ tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "session_id is required");
+ return;
+ }
+ items = malloc((tiny_store_entry_count(context->store) + 1) * sizeof(*items));
+ if (!items)
+ {
+ free(session);
+ tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "out of memory");
+ return;
+ }
+ for (index = 0; index < tiny_store_entry_count(context->store); ++index)
+ {
+ const tiny_store_entry_t *entry = tiny_store_entry(context->store, index);
+ if (!entry->deleted && strcmp(entry->message.session_id, session) == 0)
+ {
+ items[count++] = entry;
+ }
+ }
+ qsort(items, count, sizeof(*items), compare_history);
+ tiny_json_init(&json);
+ tiny_json_raw(&json, "{\"history\":[");
+ for (index = 0; index < count; ++index)
+ {
+ if (index)
+ {
+ tiny_json_raw(&json, ",");
+ }
+ tiny_mcp_tool_append_message(&json, &items[index]->message);
+ }
+ tiny_json_raw(&json, "]}");
+ tiny_mcp_tool_add_json(result, &json);
+ tiny_json_free(&json);
+ free(items);
+ free(session);
+}
+
+bool
+tiny_mcp_register_read_messages(mcpc_server_t *server)
+{
+ mcpc_tool_t *tool = tiny_mcp_tool_new("read_messages", "Read a session", call_read_messages);
+ return tool && tiny_mcp_tool_add_property(tool, "session_id", "Session ID", MCPC_U8STR)
+ && mcpc_server_add_tool(server, tool) == MCPC_EC_0;
+}
diff --git a/src/mcp/tools/rebuild_index.c b/src/mcp/tools/rebuild_index.c
new file mode 100644
index 0000000..69f8341
--- /dev/null
+++ b/src/mcp/tools/rebuild_index.c
@@ -0,0 +1,57 @@
+#include "mcp/tools/common/registry.h"
+
+#include "mcp/tools/common/common.h"
+#include "mcp/tools/common/context.h"
+
+#include "base/allocator.h"
+
+static void
+call_rebuild_index(const mcpc_tool_t *tool, mcpc_ucbr_t *result)
+{
+ tiny_mcp_tools_context_t *context = tiny_mcp_tools_context_current();
+ tiny_rebuild_report_t report;
+ tiny_search_index_t *rebuilt;
+ tiny_store_status_t status;
+ tiny_json_buffer_t json;
+ (void)tool;
+ if (!context)
+ {
+ tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "tool context is unavailable");
+ return;
+ }
+ status = tiny_store_rebuild(context->store, &report);
+ if (status != TINY_STORE_OK)
+ {
+ tiny_mcp_tool_add_error(result, tiny_store_status_name(status), "index rebuild failed");
+ return;
+ }
+ rebuilt = tiny_mcp_tools_build_search_index(context->store);
+ if (!rebuilt)
+ {
+ tiny_search_index_free(context->search_index);
+ context->search_index = NULL;
+ tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "search index allocation failed");
+ return;
+ }
+ tiny_search_index_free(context->search_index);
+ context->search_index = rebuilt;
+ tiny_json_init(&json);
+ tiny_json_raw(&json, "{\"status\":\"OK\",\"messages_indexed\":");
+ tiny_json_u64(&json, report.messages_indexed);
+ tiny_json_raw(&json, ",\"records_removed\":");
+ tiny_json_u64(&json, report.records_removed);
+ tiny_json_raw(&json, ",\"bytes_before\":");
+ tiny_json_u64(&json, report.bytes_before);
+ tiny_json_raw(&json, ",\"bytes_after\":");
+ tiny_json_u64(&json, report.bytes_after);
+ tiny_json_raw(&json, "}");
+ tiny_mcp_tool_add_json(result, &json);
+ tiny_json_free(&json);
+}
+
+bool
+tiny_mcp_register_rebuild_index(mcpc_server_t *server)
+{
+ mcpc_tool_t *tool = tiny_mcp_tool_new("rebuild_index", "Compact and rebuild", call_rebuild_index);
+ return tool && mcpc_server_add_tool(server, tool) == MCPC_EC_0;
+}
diff --git a/src/mcp/tools/search_messages.c b/src/mcp/tools/search_messages.c
new file mode 100644
index 0000000..166c167
--- /dev/null
+++ b/src/mcp/tools/search_messages.c
@@ -0,0 +1,134 @@
+#include "mcp/tools/common/registry.h"
+
+#include "mcp/tools/common/common.h"
+#include "mcp/tools/common/context.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "base/allocator.h"
+
+typedef struct result_item
+{
+ const tiny_store_entry_t *entry;
+ double score;
+} result_item_t;
+
+static int
+compare_results(const void *left, const void *right)
+{
+ const result_item_t *a = left;
+ const result_item_t *b = right;
+ if (a->score > b->score)
+ {
+ return -1;
+ }
+ if (a->score < b->score)
+ {
+ return 1;
+ }
+ if (a->entry->message.timestamp_ms > b->entry->message.timestamp_ms)
+ {
+ return -1;
+ }
+ if (a->entry->message.timestamp_ms < b->entry->message.timestamp_ms)
+ {
+ return 1;
+ }
+ return strcmp(a->entry->message.message_id, b->entry->message.message_id);
+}
+
+static void
+call_search_messages(const mcpc_tool_t *tool, mcpc_ucbr_t *result)
+{
+ tiny_mcp_tools_context_t *context = tiny_mcp_tools_context_current();
+ char *query = tiny_mcp_tool_get_string(tool, "query", TINY_QUERY_MAX);
+ char *session = tiny_mcp_tool_get_string(tool, "session_id", TINY_SESSION_ID_MAX);
+ int32_t requested_limit = 0;
+ size_t limit, index, count = 0, candidate_count = 0, *candidates = NULL;
+ result_item_t *items = NULL;
+ tiny_json_buffer_t json;
+ bool valid
+ = context && query && query[0] && session
+ && mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"limit", &requested_limit) == MCPC_EC_0
+ && requested_limit >= 1 && requested_limit <= 100;
+ if (!valid)
+ {
+ tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "query or limit is invalid");
+ }
+ else
+ {
+ limit = (size_t)requested_limit;
+ if (!context->search_index)
+ {
+ context->search_index = tiny_mcp_tools_build_search_index(context->store);
+ }
+ if (!context->search_index
+ || !tiny_search_index_candidates(
+ context->search_index, query, &candidates, &candidate_count))
+ {
+ tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "search index allocation failed");
+ }
+ else
+ {
+ items = malloc((candidate_count + 1) * sizeof(*items));
+ if (!items)
+ {
+ tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "out of memory");
+ }
+ else
+ {
+ for (index = 0; index < candidate_count; ++index)
+ {
+ const tiny_store_entry_t *entry
+ = tiny_store_entry(context->store, candidates[index]);
+ double score;
+ if (!entry || entry->deleted
+ || (session[0] && strcmp(entry->message.session_id, session) != 0))
+ {
+ continue;
+ }
+ score = tiny_search_score(query, &entry->message);
+ if (score > 0)
+ {
+ items[count].entry = entry;
+ items[count++].score = score;
+ }
+ }
+ qsort(items, count, sizeof(*items), compare_results);
+ if (count > limit)
+ {
+ count = limit;
+ }
+ tiny_json_init(&json);
+ tiny_json_raw(&json, "{\"results\":[");
+ for (index = 0; index < count; ++index)
+ {
+ if (index)
+ {
+ tiny_json_raw(&json, ",");
+ }
+ tiny_mcp_tool_append_message(&json, &items[index].entry->message);
+ }
+ tiny_json_raw(&json, "]}");
+ tiny_mcp_tool_add_json(result, &json);
+ tiny_json_free(&json);
+ }
+ }
+ }
+ free(items);
+ free(candidates);
+ free(query);
+ free(session);
+}
+
+bool
+tiny_mcp_register_search_messages(mcpc_server_t *server)
+{
+ mcpc_tool_t *tool = tiny_mcp_tool_new("search_messages", "Search messages", call_search_messages);
+ return tool && tiny_mcp_tool_add_property(tool, "query", "Search text", MCPC_U8STR)
+ && tiny_mcp_tool_add_property(tool, "session_id", "Session ID; empty for all", MCPC_U8STR)
+ && tiny_mcp_tool_add_property(tool, "limit", "Result limit from 1 to 100", MCPC_I32)
+ && mcpc_server_add_tool(server, tool) == MCPC_EC_0;
+}
diff --git a/src/mcp/tools/write_message.c b/src/mcp/tools/write_message.c
new file mode 100644
index 0000000..1d7ece2
--- /dev/null
+++ b/src/mcp/tools/write_message.c
@@ -0,0 +1,213 @@
+#include "mcp/tools/common/registry.h"
+
+#include "mcp/tools/common/common.h"
+#include "mcp/tools/common/context.h"
+#include "mjson.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "base/allocator.h"
+
+static bool
+metadata_has_only_known_fields(const char *json, int length)
+{
+ int offset = 0, key_offset, key_length, value_offset, value_length, value_type;
+ while ((offset = mjson_next(json,
+ length,
+ offset,
+ &key_offset,
+ &key_length,
+ &value_offset,
+ &value_length,
+ &value_type))
+ != 0)
+ {
+ char key[32];
+ int decoded = mjson_get_string(json + key_offset, key_length, "$", key, sizeof(key));
+ (void)value_offset;
+ (void)value_length;
+ (void)value_type;
+ if (decoded <= 0 || decoded >= (int)sizeof(key))
+ {
+ return false;
+ }
+ key[decoded] = '\0';
+ if (strcmp(key, "tool_call_id") != 0 && strcmp(key, "tags") != 0
+ && strcmp(key, "memory_tier") != 0)
+ {
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool
+parse_metadata(const char *json, tiny_message_t *message)
+{
+ const char *tool_call = NULL, *tier = NULL, *tags = NULL;
+ int tool_call_len = 0, tier_len = 0, tags_len = 0;
+ int offset = 0, key_offset, key_length, value_offset, value_length, value_type;
+ int length = (int)strlen(json);
+ int tool_call_type, tier_type, tags_type;
+ if (strcmp(json, "{}") == 0)
+ {
+ return true;
+ }
+ if (mjson_find(json, length, "$", NULL, NULL) != MJSON_TOK_OBJECT
+ || !metadata_has_only_known_fields(json, length))
+ {
+ return false;
+ }
+ tool_call_type = mjson_find(json, length, "$.tool_call_id", &tool_call, &tool_call_len);
+ tier_type = mjson_find(json, length, "$.memory_tier", &tier, &tier_len);
+ tags_type = mjson_find(json, length, "$.tags", &tags, &tags_len);
+ if ((tool_call_type != MJSON_TOK_INVALID && tool_call_type != MJSON_TOK_STRING)
+ || (tier_type != MJSON_TOK_INVALID && tier_type != MJSON_TOK_STRING)
+ || (tags_type != MJSON_TOK_INVALID && tags_type != MJSON_TOK_ARRAY))
+ {
+ return false;
+ }
+ if (tool_call_type == MJSON_TOK_STRING)
+ {
+ int decoded;
+ message->metadata.tool_call_id = malloc(TINY_TOOL_CALL_ID_MAX + 1);
+ if (!message->metadata.tool_call_id)
+ {
+ return false;
+ }
+ decoded = mjson_get_string(json,
+ length,
+ "$.tool_call_id",
+ message->metadata.tool_call_id,
+ TINY_TOOL_CALL_ID_MAX + 1);
+ if (decoded < 0 || decoded > TINY_TOOL_CALL_ID_MAX
+ || !tiny_utf8_valid(message->metadata.tool_call_id, (size_t)decoded))
+ {
+ return false;
+ }
+ message->metadata.tool_call_id[decoded] = '\0';
+ }
+ if (tier_type == MJSON_TOK_STRING)
+ {
+ char tier_value[16];
+ int decoded = mjson_get_string(json, length, "$.memory_tier", tier_value, sizeof(tier_value));
+ if (decoded < 0 || decoded >= (int)sizeof(tier_value))
+ {
+ return false;
+ }
+ tier_value[decoded] = '\0';
+ if (!tiny_tier_parse(tier_value, &message->metadata.memory_tier))
+ {
+ return false;
+ }
+ }
+ if (tags_type == MJSON_TOK_ARRAY)
+ {
+ while ((offset = mjson_next(tags,
+ tags_len,
+ offset,
+ &key_offset,
+ &key_length,
+ &value_offset,
+ &value_length,
+ &value_type))
+ != 0)
+ {
+ char *tag;
+ char **resized;
+ int decoded;
+ if (value_type != MJSON_TOK_STRING || message->metadata.tag_count >= TINY_TAG_COUNT_MAX)
+ {
+ return false;
+ }
+ tag = malloc(TINY_TAG_MAX + 1);
+ if (!tag)
+ {
+ return false;
+ }
+ decoded = mjson_get_string(tags + value_offset, value_length, "$", tag, TINY_TAG_MAX + 1);
+ if (decoded <= 0 || decoded > TINY_TAG_MAX || !tiny_utf8_valid(tag, (size_t)decoded))
+ {
+ free(tag);
+ return false;
+ }
+ tag[decoded] = '\0';
+ resized = realloc(message->metadata.tags,
+ (message->metadata.tag_count + 1) * sizeof(*message->metadata.tags));
+ if (!resized)
+ {
+ free(tag);
+ return false;
+ }
+ message->metadata.tags = resized;
+ message->metadata.tags[message->metadata.tag_count++] = tag;
+ }
+ }
+ return true;
+}
+
+static void
+call_write_message(const mcpc_tool_t *tool, mcpc_ucbr_t *result)
+{
+ tiny_mcp_tools_context_t *context = tiny_mcp_tools_context_current();
+ tiny_message_t input, written;
+ tiny_store_status_t status;
+ char *role = NULL, *metadata = NULL;
+ tiny_json_buffer_t json;
+ tiny_message_init(&input);
+ tiny_message_init(&written);
+ input.session_id = tiny_mcp_tool_get_string(tool, "session_id", TINY_SESSION_ID_MAX);
+ role = tiny_mcp_tool_get_string(tool, "role", 16);
+ input.content = tiny_mcp_tool_get_string(tool, "content", TINY_CONTENT_MAX);
+ metadata = tiny_mcp_tool_get_string(tool, "metadata_json", TINY_CONTENT_MAX);
+ bool valid = context && input.session_id && input.session_id[0] && role && input.content
+ && input.content[0] && metadata && tiny_role_parse(role, &input.role)
+ && parse_metadata(metadata, &input);
+ if (!valid)
+ {
+ tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "invalid write_message arguments");
+ }
+ else
+ {
+ status = tiny_store_write(context->store, &input, &written);
+ if (status != TINY_STORE_OK)
+ {
+ tiny_mcp_tool_add_error(
+ result, tiny_store_status_name(status), "message was not written");
+ }
+ else
+ {
+ if (context->search_index
+ && !tiny_search_index_add(
+ context->search_index, &written, tiny_store_entry_count(context->store) - 1))
+ {
+ tiny_search_index_free(context->search_index);
+ context->search_index = NULL;
+ }
+ tiny_json_init(&json);
+ tiny_json_raw(&json, "{\"status\":\"OK\",\"message\":");
+ tiny_mcp_tool_append_message(&json, &written);
+ tiny_json_raw(&json, "}");
+ tiny_mcp_tool_add_json(result, &json);
+ tiny_json_free(&json);
+ }
+ }
+ free(role);
+ free(metadata);
+ tiny_message_free(&input);
+ tiny_message_free(&written);
+}
+
+bool
+tiny_mcp_register_write_message(mcpc_server_t *server)
+{
+ mcpc_tool_t *tool
+ = tiny_mcp_tool_new("write_message", "Append a conversation message", call_write_message);
+ return tool && tiny_mcp_tool_add_property(tool, "session_id", "Session ID", MCPC_U8STR)
+ && tiny_mcp_tool_add_property(tool, "role", "user, assistant, or system", MCPC_U8STR)
+ && tiny_mcp_tool_add_property(tool, "content", "Message text", MCPC_U8STR)
+ && tiny_mcp_tool_add_property(
+ tool, "metadata_json", "Metadata object JSON; use {} when empty", MCPC_U8STR)
+ && mcpc_server_add_tool(server, tool) == MCPC_EC_0;
+}
diff --git a/src/storage/README.md b/src/storage/README.md
index 3c03dba..79874b0 100644
--- a/src/storage/README.md
+++ b/src/storage/README.md
@@ -2,7 +2,8 @@
The storage adapter owns `active.log`, `snapshot.bin`, the data-directory lock,
in-memory live/deleted state, synchronous PUT/DEL writes, startup replay, and
-compaction. It uses Win32 64-bit file APIs and `FlushFileBuffers`.
+compaction. It depends only on the UTF-8, opaque-handle API in `src/sys`; OS
+file and durability primitives do not leak into this layer.
The binary format is private but versioned. Invalid complete records are never
skipped; only an incomplete EOF record is truncated. Snapshot generation must
diff --git a/src/storage/codec.c b/src/storage/codec.c
new file mode 100644
index 0000000..af061ce
--- /dev/null
+++ b/src/storage/codec.c
@@ -0,0 +1,76 @@
+#include "storage/codec.h"
+
+void
+tiny_codec_put_u32(unsigned char *output, uint32_t value)
+{
+ if (output == NULL)
+ {
+ return;
+ }
+ output[0] = (unsigned char)value;
+ output[1] = (unsigned char)(value >> 8);
+ output[2] = (unsigned char)(value >> 16);
+ output[3] = (unsigned char)(value >> 24);
+}
+
+void
+tiny_codec_put_u64(unsigned char *output, uint64_t value)
+{
+ unsigned index;
+ if (output == NULL)
+ {
+ return;
+ }
+ for (index = 0; index < 8; ++index)
+ {
+ output[index] = (unsigned char)(value >> (index * 8));
+ }
+}
+
+uint32_t
+tiny_codec_get_u32(const unsigned char *input)
+{
+ if (input == NULL)
+ {
+ return 0;
+ }
+ return (uint32_t)input[0] | ((uint32_t)input[1] << 8) | ((uint32_t)input[2] << 16)
+ | ((uint32_t)input[3] << 24);
+}
+
+uint64_t
+tiny_codec_get_u64(const unsigned char *input)
+{
+ uint64_t value = 0;
+ int index;
+ if (input == NULL)
+ {
+ return 0;
+ }
+ for (index = 7; index >= 0; --index)
+ {
+ value = (value << 8) | input[index];
+ }
+ return value;
+}
+
+uint32_t
+tiny_codec_crc32(const unsigned char *data, size_t length)
+{
+ uint32_t crc = 0xffffffffU;
+ size_t index;
+ if (data == NULL && length != 0)
+ {
+ return 0;
+ }
+ for (index = 0; index < length; ++index)
+ {
+ unsigned bit;
+ crc ^= data[index];
+ for (bit = 0; bit < 8; ++bit)
+ {
+ crc = (crc >> 1) ^ (0xedb88320U & (uint32_t)-(int32_t)(crc & 1));
+ }
+ }
+ return ~crc;
+}
diff --git a/src/storage/codec.h b/src/storage/codec.h
new file mode 100644
index 0000000..aafd174
--- /dev/null
+++ b/src/storage/codec.h
@@ -0,0 +1,13 @@
+#ifndef TINY_CONVMEM_STORAGE_CODEC_H
+#define TINY_CONVMEM_STORAGE_CODEC_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+void tiny_codec_put_u32(unsigned char *output, uint32_t value);
+void tiny_codec_put_u64(unsigned char *output, uint64_t value);
+uint32_t tiny_codec_get_u32(const unsigned char *input);
+uint64_t tiny_codec_get_u64(const unsigned char *input);
+uint32_t tiny_codec_crc32(const unsigned char *data, size_t length);
+
+#endif
diff --git a/src/storage/store.c b/src/storage/store.c
index 9d26e0f..296f4d0 100644
--- a/src/storage/store.c
+++ b/src/storage/store.c
@@ -1,13 +1,13 @@
-#define WIN32_LEAN_AND_MEAN
-#include <windows.h>
-#include <bcrypt.h>
-
#include "storage/store.h"
+#include "storage/codec.h"
+#include "sys/system.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include "base/allocator.h"
+
#define RECORD_HEADER_SIZE 24U
#define RECORD_VERSION 1U
#define RECORD_PUT 1U
@@ -18,11 +18,11 @@
struct tiny_store
{
- HANDLE lock;
- HANDLE log;
- wchar_t *directory;
- wchar_t *log_path;
- wchar_t *snapshot_path;
+ tiny_sys_file_t lock;
+ tiny_sys_file_t log;
+ char *directory;
+ char *log_path;
+ char *snapshot_path;
tiny_store_entry_t *entries;
size_t entry_count;
size_t entry_capacity;
@@ -37,89 +37,19 @@ struct tiny_store
};
static void
-put_u32(unsigned char *p, uint32_t v)
-{
- p[0] = (unsigned char)v;
- p[1] = (unsigned char)(v >> 8);
- p[2] = (unsigned char)(v >> 16);
- p[3] = (unsigned char)(v >> 24);
-}
-static void
-put_u64(unsigned char *p, uint64_t v)
-{
- unsigned i;
- for (i = 0; i < 8; ++i)
- {
- p[i] = (unsigned char)(v >> (i * 8));
- }
-}
-static uint32_t
-get_u32(const unsigned char *p)
-{
- return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
-}
-static uint64_t
-get_u64(const unsigned char *p)
-{
- uint64_t v = 0;
- int i;
- for (i = 7; i >= 0; --i)
- {
- v = (v << 8) | p[i];
- }
- return v;
-}
-
-static uint32_t
-crc32_bytes(const unsigned char *data, size_t length)
-{
- uint32_t crc = 0xffffffffU;
- size_t i;
- for (i = 0; i < length; ++i)
- {
- unsigned bit;
- crc ^= data[i];
- for (bit = 0; bit < 8; ++bit)
- {
- crc = (crc >> 1) ^ (0xedb88320U & (uint32_t)-(int32_t)(crc & 1));
- }
- }
- return ~crc;
-}
-
-static wchar_t *
-join_path(const wchar_t *directory, const wchar_t *name)
-{
- const size_t left = wcslen(directory), right = wcslen(name);
- const bool separator = left > 0 && directory[left - 1] != L'\\' && directory[left - 1] != L'/';
- wchar_t *result = malloc((left + right + (separator ? 2 : 1)) * sizeof(wchar_t));
- if (result == NULL)
- {
- return NULL;
- }
- wcscpy_s(result, left + right + (separator ? 2 : 1), directory);
- if (separator)
- {
- wcscat_s(result, left + right + 2, L"\\");
- }
- wcscat_s(result, left + right + (separator ? 2 : 1), name);
- return result;
-}
-
-static void
set_error(tiny_store_t *store, const char *message)
{
- strncpy_s(store->last_error, sizeof(store->last_error), message, _TRUNCATE);
+ snprintf(store->last_error, sizeof(store->last_error), "%s", message);
}
static bool
-write_all(HANDLE file, const void *data, uint32_t length)
+write_all(tiny_sys_file_t *file, const void *data, uint32_t length)
{
const unsigned char *position = data;
while (length > 0)
{
- DWORD written = 0;
- if (!WriteFile(file, position, length, &written, NULL) || written == 0)
+ uint32_t written = 0;
+ if (!tiny_sys_file_write(file, position, length, &written) || written == 0)
{
return false;
}
@@ -130,14 +60,14 @@ write_all(HANDLE file, const void *data, uint32_t length)
}
static bool
-read_all(HANDLE file, void *data, uint32_t length, uint32_t *actual)
+read_all(tiny_sys_file_t *file, void *data, uint32_t length, uint32_t *actual)
{
unsigned char *position = data;
uint32_t total = 0;
while (total < length)
{
- DWORD count = 0;
- if (!ReadFile(file, position + total, length - total, &count, NULL))
+ uint32_t count = 0;
+ if (!tiny_sys_file_read(file, position + total, length - total, &count))
{
return false;
}
@@ -154,12 +84,7 @@ read_all(HANDLE file, void *data, uint32_t length, uint32_t *actual)
static uint64_t
now_milliseconds(void)
{
- FILETIME ft;
- ULARGE_INTEGER ticks;
- GetSystemTimeAsFileTime(&ft);
- ticks.LowPart = ft.dwLowDateTime;
- ticks.HighPart = ft.dwHighDateTime;
- return ticks.QuadPart / 10000ULL - 11644473600000ULL;
+ return tiny_sys_now_milliseconds();
}
static void
@@ -171,11 +96,9 @@ make_ulid(tiny_store_t *store, uint64_t timestamp, char output[27])
if (timestamp > store->last_ulid_ms)
{
store->last_ulid_ms = timestamp;
- if (BCryptGenRandom(
- NULL, store->last_random, sizeof(store->last_random), BCRYPT_USE_SYSTEM_PREFERRED_RNG)
- != 0)
+ if (!tiny_sys_random_bytes(store->last_random, sizeof(store->last_random)))
{
- uint64_t fallback = GetTickCount64() ^ timestamp;
+ uint64_t fallback = tiny_sys_monotonic_milliseconds() ^ timestamp;
for (index = 0; index < sizeof(store->last_random); ++index)
{
store->last_random[index] = (unsigned char)(fallback >> ((index % 8) * 8));
@@ -274,19 +197,19 @@ serialize_put(const tiny_message_t *message, unsigned char **payload, uint32_t *
return false;
}
offset = 0;
- put_u64(data + offset, (uint64_t)message->timestamp_ms);
+ tiny_codec_put_u64(data + offset, (uint64_t)message->timestamp_ms);
offset += 8;
memcpy(data + offset, message->message_id, 26);
offset += 26;
data[offset++] = (unsigned char)message->role;
data[offset++] = (unsigned char)message->metadata.memory_tier;
- put_u32(data + offset, (uint32_t)session_len);
+ tiny_codec_put_u32(data + offset, (uint32_t)session_len);
offset += 4;
- put_u32(data + offset, (uint32_t)content_len);
+ tiny_codec_put_u32(data + offset, (uint32_t)content_len);
offset += 4;
- put_u32(data + offset, (uint32_t)tool_len);
+ tiny_codec_put_u32(data + offset, (uint32_t)tool_len);
offset += 4;
- put_u32(data + offset, (uint32_t)message->metadata.tag_count);
+ tiny_codec_put_u32(data + offset, (uint32_t)message->metadata.tag_count);
offset += 4;
memcpy(data + offset, message->session_id, session_len);
offset += session_len;
@@ -300,7 +223,7 @@ serialize_put(const tiny_message_t *message, unsigned char **payload, uint32_t *
for (index = 0; index < message->metadata.tag_count; ++index)
{
const size_t tag_len = strlen(message->metadata.tags[index]);
- put_u32(data + offset, (uint32_t)tag_len);
+ tiny_codec_put_u32(data + offset, (uint32_t)tag_len);
offset += 4;
memcpy(data + offset, message->metadata.tags[index], tag_len);
offset += tag_len;
@@ -323,6 +246,13 @@ copy_field(const unsigned char *data, size_t length)
}
static bool
+deserialize_failed(tiny_message_t *message)
+{
+ tiny_message_free(message);
+ return false;
+}
+
+static bool
deserialize_put(const unsigned char *data, uint32_t length, tiny_message_t *message)
{
uint32_t session_len, content_len, tool_len, tag_count;
@@ -332,20 +262,20 @@ deserialize_put(const unsigned char *data, uint32_t length, tiny_message_t *mess
{
return false;
}
- message->timestamp_ms = (int64_t)get_u64(data + offset);
+ message->timestamp_ms = (int64_t)tiny_codec_get_u64(data + offset);
offset += 8;
memcpy(message->message_id, data + offset, 26);
message->message_id[26] = '\0';
offset += 26;
message->role = (tiny_role_t)data[offset++];
message->metadata.memory_tier = (tiny_memory_tier_t)data[offset++];
- session_len = get_u32(data + offset);
+ session_len = tiny_codec_get_u32(data + offset);
offset += 4;
- content_len = get_u32(data + offset);
+ content_len = tiny_codec_get_u32(data + offset);
offset += 4;
- tool_len = get_u32(data + offset);
+ tool_len = tiny_codec_get_u32(data + offset);
offset += 4;
- tag_count = get_u32(data + offset);
+ tag_count = tiny_codec_get_u32(data + offset);
offset += 4;
if (!tiny_message_id_valid(message->message_id)
|| (message->role < TINY_ROLE_USER || message->role > TINY_ROLE_SYSTEM)
@@ -369,33 +299,33 @@ deserialize_put(const unsigned char *data, uint32_t length, tiny_message_t *mess
if (message->session_id == NULL || message->content == NULL
|| (tool_len && !message->metadata.tool_call_id))
{
- goto fail;
+ return deserialize_failed(message);
}
if (tag_count > 0)
{
message->metadata.tags = calloc(tag_count, sizeof(char *));
if (message->metadata.tags == NULL)
{
- goto fail;
+ return deserialize_failed(message);
}
for (index = 0; index < tag_count; ++index)
{
uint32_t tag_len;
if (offset + 4 > length)
{
- goto fail;
+ return deserialize_failed(message);
}
- tag_len = get_u32(data + offset);
+ tag_len = tiny_codec_get_u32(data + offset);
offset += 4;
if (tag_len == 0 || tag_len > TINY_TAG_MAX || offset + tag_len > length)
{
- goto fail;
+ return deserialize_failed(message);
}
message->metadata.tags[index] = copy_field(data + offset, tag_len);
offset += tag_len;
if (message->metadata.tags[index] == NULL)
{
- goto fail;
+ return deserialize_failed(message);
}
message->metadata.tag_count++;
}
@@ -403,16 +333,13 @@ deserialize_put(const unsigned char *data, uint32_t length, tiny_message_t *mess
if (offset != length || !tiny_utf8_valid(message->session_id, session_len)
|| !tiny_utf8_valid(message->content, content_len))
{
- goto fail;
+ return deserialize_failed(message);
}
return true;
-fail:
- tiny_message_free(message);
- return false;
}
static tiny_store_status_t
-append_record_to(HANDLE file,
+append_record_to(tiny_sys_file_t *file,
uint8_t type,
uint64_t sequence,
const unsigned char *payload,
@@ -420,16 +347,14 @@ append_record_to(HANDLE file,
bool flush)
{
unsigned char header[RECORD_HEADER_SIZE] = { 0 };
- LARGE_INTEGER end;
memcpy(header, "TCMR", 4);
header[4] = RECORD_VERSION;
header[5] = type;
- put_u32(header + 8, payload_length);
- put_u64(header + 12, sequence);
- put_u32(header + 20, crc32_bytes(payload, payload_length));
- end.QuadPart = 0;
- if (!SetFilePointerEx(file, end, NULL, FILE_END) || !write_all(file, header, sizeof(header))
- || !write_all(file, payload, payload_length) || (flush && !FlushFileBuffers(file)))
+ tiny_codec_put_u32(header + 8, payload_length);
+ tiny_codec_put_u64(header + 12, sequence);
+ tiny_codec_put_u32(header + 20, tiny_codec_crc32(payload, payload_length));
+ if (!tiny_sys_file_seek(file, 0, TINY_SYS_SEEK_END) || !write_all(file, header, sizeof(header))
+ || !write_all(file, payload, payload_length) || (flush && !tiny_sys_file_flush(file)))
{
return TINY_STORE_IO_ERROR;
}
@@ -469,38 +394,51 @@ clear_entries(tiny_store_t *store)
}
static tiny_store_status_t
+recover_torn_tail(tiny_store_t *store, uint64_t boundary)
+{
+ if (boundary > INT64_MAX
+ || !tiny_sys_file_seek(&store->log, (int64_t)boundary, TINY_SYS_SEEK_BEGIN)
+ || !tiny_sys_file_truncate(&store->log) || !tiny_sys_file_flush(&store->log))
+ {
+ return TINY_STORE_IO_ERROR;
+ }
+ store->recovered_tail = true;
+ set_error(store, "an incomplete trailing record was truncated during recovery");
+ return TINY_STORE_OK;
+}
+
+static tiny_store_status_t
replay_log(tiny_store_t *store, uint64_t starting_offset)
{
- LARGE_INTEGER start, file_size, boundary;
- start.QuadPart = (LONGLONG)starting_offset;
- boundary = start;
- if (!SetFilePointerEx(store->log, start, NULL, FILE_BEGIN)
- || !GetFileSizeEx(store->log, &file_size))
+ uint64_t file_size, boundary = starting_offset;
+ if (starting_offset > INT64_MAX
+ || !tiny_sys_file_seek(&store->log, (int64_t)starting_offset, TINY_SYS_SEEK_BEGIN)
+ || !tiny_sys_file_size(&store->log, &file_size))
{
return TINY_STORE_IO_ERROR;
}
- while (boundary.QuadPart < file_size.QuadPart)
+ while (boundary < file_size)
{
unsigned char header[RECORD_HEADER_SIZE], *payload = NULL;
uint32_t actual = 0, payload_length, expected_crc;
uint64_t sequence;
tiny_store_status_t status = TINY_STORE_OK;
- if (!read_all(store->log, header, sizeof(header), &actual))
+ if (!read_all(&store->log, header, sizeof(header), &actual))
{
return TINY_STORE_IO_ERROR;
}
if (actual < sizeof(header))
{
- goto torn_tail;
+ return recover_torn_tail(store, boundary);
}
if (memcmp(header, "TCMR", 4) != 0 || header[4] != RECORD_VERSION
|| (header[5] != RECORD_PUT && header[5] != RECORD_DEL && header[5] != RECORD_MARKER))
{
return TINY_STORE_CORRUPT;
}
- payload_length = get_u32(header + 8);
- sequence = get_u64(header + 12);
- expected_crc = get_u32(header + 20);
+ payload_length = tiny_codec_get_u32(header + 8);
+ sequence = tiny_codec_get_u64(header + 12);
+ expected_crc = tiny_codec_get_u32(header + 20);
if (payload_length > RECORD_PAYLOAD_MAX
|| (header[5] != RECORD_MARKER && sequence <= store->last_sequence))
{
@@ -511,7 +449,7 @@ replay_log(tiny_store_t *store, uint64_t starting_offset)
{
return TINY_STORE_NO_MEMORY;
}
- if (!read_all(store->log, payload, payload_length, &actual))
+ if (!read_all(&store->log, payload, payload_length, &actual))
{
free(payload);
return TINY_STORE_IO_ERROR;
@@ -519,53 +457,62 @@ replay_log(tiny_store_t *store, uint64_t starting_offset)
if (actual < payload_length)
{
free(payload);
- goto torn_tail;
+ return recover_torn_tail(store, boundary);
}
- if (crc32_bytes(payload, payload_length) != expected_crc)
+ if (tiny_codec_crc32(payload, payload_length) != expected_crc)
{
free(payload);
return TINY_STORE_CORRUPT;
}
- if (header[5] == RECORD_MARKER)
+ switch (header[5])
{
- if (boundary.QuadPart != 0 || sequence != 0
- || payload_length != sizeof(store->generation))
+ case RECORD_MARKER:
+ if (boundary != 0 || sequence != 0 || payload_length != sizeof(store->generation))
{
free(payload);
return TINY_STORE_CORRUPT;
}
memcpy(store->generation, payload, sizeof(store->generation));
store->has_generation = true;
- }
- else if (header[5] == RECORD_PUT)
- {
- tiny_message_t message;
- if (!deserialize_put(payload, payload_length, &message))
- {
- free(payload);
- return TINY_STORE_CORRUPT;
- }
- status = apply_put(store, &message, sequence);
- tiny_message_free(&message);
- }
- else
- {
- char id[27];
- tiny_store_entry_t *entry;
- if (payload_length != 34)
- {
- free(payload);
- return TINY_STORE_CORRUPT;
- }
- memcpy(id, payload, 26);
- id[26] = '\0';
- entry = find_entry(store, id);
- if (entry == NULL || entry->deleted || !tiny_message_id_valid(id))
- {
- free(payload);
- return TINY_STORE_CORRUPT;
- }
- entry->deleted = true;
+ break;
+
+ case RECORD_PUT:
+ {
+ tiny_message_t message;
+ if (!deserialize_put(payload, payload_length, &message))
+ {
+ free(payload);
+ return TINY_STORE_CORRUPT;
+ }
+ status = apply_put(store, &message, sequence);
+ tiny_message_free(&message);
+ }
+ break;
+
+ case RECORD_DEL:
+ {
+ char id[27];
+ tiny_store_entry_t *entry;
+ if (payload_length != 34)
+ {
+ free(payload);
+ return TINY_STORE_CORRUPT;
+ }
+ memcpy(id, payload, 26);
+ id[26] = '\0';
+ entry = find_entry(store, id);
+ if (entry == NULL || entry->deleted || !tiny_message_id_valid(id))
+ {
+ free(payload);
+ return TINY_STORE_CORRUPT;
+ }
+ entry->deleted = true;
+ }
+ break;
+
+ default:
+ free(payload);
+ return TINY_STORE_CORRUPT;
}
free(payload);
if (status != TINY_STORE_OK)
@@ -576,49 +523,43 @@ replay_log(tiny_store_t *store, uint64_t starting_offset)
{
store->last_sequence = sequence;
}
- boundary.QuadPart += RECORD_HEADER_SIZE + payload_length;
+ boundary += RECORD_HEADER_SIZE + payload_length;
}
return TINY_STORE_OK;
-
-torn_tail:
- if (!SetFilePointerEx(store->log, boundary, NULL, FILE_BEGIN) || !SetEndOfFile(store->log)
- || !FlushFileBuffers(store->log))
- {
- return TINY_STORE_IO_ERROR;
- }
- store->recovered_tail = true;
- set_error(store, "an incomplete trailing record was truncated during recovery");
- return TINY_STORE_OK;
}
static tiny_store_status_t
write_snapshot(tiny_store_t *store)
{
- wchar_t *temporary = join_path(store->directory, L"snapshot.tmp");
- HANDLE file;
+ char *temporary = tiny_sys_path_join(store->directory, "snapshot.tmp");
+ tiny_sys_file_t file;
unsigned char data[SNAPSHOT_HEADER_SIZE] = { 0 };
- LARGE_INTEGER size;
+ uint64_t size;
bool ok;
size_t index;
if (temporary == NULL)
{
return TINY_STORE_NO_MEMORY;
}
- file = CreateFileW(temporary, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
- if (file == INVALID_HANDLE_VALUE)
+ if (tiny_sys_file_open(&file, temporary, TINY_SYS_FILE_WRITE_NEW) != TINY_SYS_OK)
{
free(temporary);
return TINY_STORE_IO_ERROR;
}
- GetFileSizeEx(store->log, &size);
+ if (!tiny_sys_file_size(&store->log, &size))
+ {
+ tiny_sys_file_close(&file);
+ free(temporary);
+ return TINY_STORE_IO_ERROR;
+ }
memcpy(data, "TCMS", 4);
data[4] = 1;
memcpy(data + 8, store->generation, 16);
- put_u64(data + 24, store->last_sequence);
- put_u64(data + 32, (uint64_t)size.QuadPart);
- put_u64(data + 40, (uint64_t)store->entry_count);
- put_u32(data + 48, crc32_bytes(data, 48));
- ok = write_all(file, data, sizeof(data));
+ tiny_codec_put_u64(data + 24, store->last_sequence);
+ tiny_codec_put_u64(data + 32, size);
+ tiny_codec_put_u64(data + 40, (uint64_t)store->entry_count);
+ tiny_codec_put_u32(data + 48, tiny_codec_crc32(data, 48));
+ ok = write_all(&file, data, sizeof(data));
for (index = 0; ok && index < store->entry_count; ++index)
{
unsigned char prefix[16], *payload = NULL;
@@ -628,22 +569,20 @@ write_snapshot(tiny_store_t *store)
ok = false;
break;
}
- put_u64(prefix, store->entries[index].sequence);
- put_u32(prefix + 8, length);
- put_u32(prefix + 12, crc32_bytes(payload, length));
- ok = write_all(file, prefix, sizeof(prefix)) && write_all(file, payload, length);
+ tiny_codec_put_u64(prefix, store->entries[index].sequence);
+ tiny_codec_put_u32(prefix + 8, length);
+ tiny_codec_put_u32(prefix + 12, tiny_codec_crc32(payload, length));
+ ok = write_all(&file, prefix, sizeof(prefix)) && write_all(&file, payload, length);
free(payload);
}
if (ok)
{
- ok = FlushFileBuffers(file) != 0;
+ ok = tiny_sys_file_flush(&file);
}
- CloseHandle(file);
+ tiny_sys_file_close(&file);
if (ok)
{
- ok = MoveFileExW(
- temporary, store->snapshot_path, MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)
- != 0;
+ ok = tiny_sys_path_replace(temporary, store->snapshot_path);
}
free(temporary);
store->snapshot_valid = ok;
@@ -655,13 +594,12 @@ log_generation_matches(tiny_store_t *store, const unsigned char generation[16])
{
unsigned char header[RECORD_HEADER_SIZE], payload[16];
uint32_t actual = 0;
- LARGE_INTEGER zero = { 0 };
- if (!SetFilePointerEx(store->log, zero, NULL, FILE_BEGIN)
- || !read_all(store->log, header, sizeof(header), &actual) || actual != sizeof(header)
+ if (!tiny_sys_file_seek(&store->log, 0, TINY_SYS_SEEK_BEGIN)
+ || !read_all(&store->log, header, sizeof(header), &actual) || actual != sizeof(header)
|| memcmp(header, "TCMR", 4) != 0 || header[4] != RECORD_VERSION || header[5] != RECORD_MARKER
- || get_u64(header + 12) != 0 || get_u32(header + 8) != 16
- || !read_all(store->log, payload, sizeof(payload), &actual) || actual != sizeof(payload)
- || get_u32(header + 20) != crc32_bytes(payload, sizeof(payload)))
+ || tiny_codec_get_u64(header + 12) != 0 || tiny_codec_get_u32(header + 8) != 16
+ || !read_all(&store->log, payload, sizeof(payload), &actual) || actual != sizeof(payload)
+ || tiny_codec_get_u32(header + 20) != tiny_codec_crc32(payload, sizeof(payload)))
{
return false;
}
@@ -669,36 +607,43 @@ log_generation_matches(tiny_store_t *store, const unsigned char generation[16])
}
static bool
+snapshot_invalid(tiny_store_t *store, tiny_sys_file_t *file, bool clear_loaded_entries)
+{
+ if (clear_loaded_entries)
+ {
+ clear_entries(store);
+ store->has_generation = false;
+ }
+ set_error(store, "snapshot validation failed; the active log was replayed from the beginning");
+ tiny_sys_file_close(file);
+ return false;
+}
+
+static bool
load_snapshot(tiny_store_t *store, uint64_t *log_offset)
{
- HANDLE file = CreateFileW(store->snapshot_path,
- GENERIC_READ,
- FILE_SHARE_READ,
- NULL,
- OPEN_EXISTING,
- FILE_ATTRIBUTE_NORMAL,
- NULL);
+ tiny_sys_file_t file;
unsigned char header[SNAPSHOT_HEADER_SIZE];
uint32_t actual = 0;
uint64_t count, index, expected_sequence, offset;
- LARGE_INTEGER log_size;
- if (file == INVALID_HANDLE_VALUE)
+ uint64_t log_size;
+ if (tiny_sys_file_open(&file, store->snapshot_path, TINY_SYS_FILE_READ) != TINY_SYS_OK)
{
return false;
}
- if (!read_all(file, header, sizeof(header), &actual) || actual != sizeof(header)
+ if (!read_all(&file, header, sizeof(header), &actual) || actual != sizeof(header)
|| memcmp(header, "TCMS", 4) != 0 || header[4] != 1
- || get_u32(header + 48) != crc32_bytes(header, 48) || !GetFileSizeEx(store->log, &log_size))
+ || tiny_codec_get_u32(header + 48) != tiny_codec_crc32(header, 48)
+ || !tiny_sys_file_size(&store->log, &log_size))
{
- goto invalid;
+ return snapshot_invalid(store, &file, false);
}
- expected_sequence = get_u64(header + 24);
- offset = get_u64(header + 32);
- count = get_u64(header + 40);
- if (offset > (uint64_t)log_size.QuadPart || count > SIZE_MAX
- || !log_generation_matches(store, header + 8))
+ expected_sequence = tiny_codec_get_u64(header + 24);
+ offset = tiny_codec_get_u64(header + 32);
+ count = tiny_codec_get_u64(header + 40);
+ if (offset > log_size || count > SIZE_MAX || !log_generation_matches(store, header + 8))
{
- goto invalid;
+ return snapshot_invalid(store, &file, false);
}
memcpy(store->generation, header + 8, 16);
store->has_generation = true;
@@ -708,99 +653,102 @@ load_snapshot(tiny_store_t *store, uint64_t *log_offset)
uint32_t length, crc;
tiny_message_t message;
tiny_store_status_t status;
- if (!read_all(file, prefix, sizeof(prefix), &actual) || actual != sizeof(prefix))
+ if (!read_all(&file, prefix, sizeof(prefix), &actual) || actual != sizeof(prefix))
{
- goto invalid_entries;
+ return snapshot_invalid(store, &file, true);
}
- length = get_u32(prefix + 8);
- crc = get_u32(prefix + 12);
+ length = tiny_codec_get_u32(prefix + 8);
+ crc = tiny_codec_get_u32(prefix + 12);
if (length > RECORD_PAYLOAD_MAX)
{
- goto invalid_entries;
+ return snapshot_invalid(store, &file, true);
}
payload = malloc(length ? length : 1);
if (!payload)
{
- goto invalid_entries;
+ return snapshot_invalid(store, &file, true);
}
- if (!read_all(file, payload, length, &actual) || actual != length
- || crc32_bytes(payload, length) != crc || !deserialize_put(payload, length, &message))
+ if (!read_all(&file, payload, length, &actual) || actual != length
+ || tiny_codec_crc32(payload, length) != crc
+ || !deserialize_put(payload, length, &message))
{
free(payload);
- goto invalid_entries;
+ return snapshot_invalid(store, &file, true);
}
free(payload);
- status = apply_put(store, &message, get_u64(prefix));
+ status = apply_put(store, &message, tiny_codec_get_u64(prefix));
tiny_message_free(&message);
if (status != TINY_STORE_OK)
{
- goto invalid_entries;
+ return snapshot_invalid(store, &file, true);
}
- store->last_sequence = get_u64(prefix);
+ store->last_sequence = tiny_codec_get_u64(prefix);
}
if (store->last_sequence != expected_sequence)
{
- goto invalid_entries;
+ return snapshot_invalid(store, &file, true);
}
- CloseHandle(file);
+ tiny_sys_file_close(&file);
*log_offset = offset;
store->snapshot_valid = true;
return true;
-invalid_entries:
- clear_entries(store);
- store->has_generation = false;
-invalid:
- set_error(store, "snapshot validation failed; the active log was replayed from the beginning");
- CloseHandle(file);
- return false;
+}
+
+static tiny_store_status_t
+store_open_failed(tiny_store_t *store, char *lock_path, tiny_store_status_t status)
+{
+ free(lock_path);
+ tiny_store_close(store);
+ return status;
}
tiny_store_status_t
-tiny_store_open(tiny_store_t **output, const wchar_t *directory)
+tiny_store_open(tiny_store_t **output, const char *directory)
{
- tiny_store_t *store = calloc(1, sizeof(*store));
- wchar_t *lock_path = NULL;
+ tiny_store_t *store;
+ char *lock_path = NULL;
+ tiny_sys_status_t sys_status;
tiny_store_status_t status;
+ if (output == NULL)
+ {
+ return TINY_STORE_IO_ERROR;
+ }
+ *output = NULL;
+ if (directory == NULL || directory[0] == '\0')
+ {
+ return TINY_STORE_IO_ERROR;
+ }
+ store = calloc(1, sizeof(*store));
if (store == NULL)
{
return TINY_STORE_NO_MEMORY;
}
- store->lock = INVALID_HANDLE_VALUE;
- store->log = INVALID_HANDLE_VALUE;
- store->directory = _wcsdup(directory);
- store->log_path = join_path(directory, L"active.log");
- store->snapshot_path = join_path(directory, L"snapshot.bin");
- lock_path = join_path(directory, L"store.lock");
+ tiny_sys_file_init(&store->lock);
+ tiny_sys_file_init(&store->log);
+ store->directory = copy_field((const unsigned char *)directory, strlen(directory));
+ store->log_path = tiny_sys_path_join(directory, "active.log");
+ store->snapshot_path = tiny_sys_path_join(directory, "snapshot.bin");
+ lock_path = tiny_sys_path_join(directory, "store.lock");
if (!store->directory || !store->log_path || !store->snapshot_path || !lock_path)
{
- status = TINY_STORE_NO_MEMORY;
- goto fail;
+ return store_open_failed(store, lock_path, TINY_STORE_NO_MEMORY);
}
- if (!CreateDirectoryW(directory, NULL) && GetLastError() != ERROR_ALREADY_EXISTS)
+ if (tiny_sys_directory_create(directory) != TINY_SYS_OK)
{
- status = TINY_STORE_IO_ERROR;
- goto fail;
+ return store_open_failed(store, lock_path, TINY_STORE_IO_ERROR);
}
- store->lock = CreateFileW(
- lock_path, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
+ sys_status = tiny_sys_file_open(&store->lock, lock_path, TINY_SYS_FILE_LOCK);
free(lock_path);
lock_path = NULL;
- if (store->lock == INVALID_HANDLE_VALUE)
+ if (sys_status != TINY_SYS_OK)
{
- status = GetLastError() == ERROR_SHARING_VIOLATION ? TINY_STORE_BUSY : TINY_STORE_IO_ERROR;
- goto fail;
+ status = sys_status == TINY_SYS_BUSY ? TINY_STORE_BUSY : TINY_STORE_IO_ERROR;
+ return store_open_failed(store, lock_path, status);
}
- store->log = CreateFileW(store->log_path,
- GENERIC_READ | GENERIC_WRITE,
- FILE_SHARE_READ,
- NULL,
- OPEN_ALWAYS,
- FILE_ATTRIBUTE_NORMAL,
- NULL);
- if (store->log == INVALID_HANDLE_VALUE)
+ if (tiny_sys_file_open(&store->log, store->log_path, TINY_SYS_FILE_READ_WRITE_CREATE)
+ != TINY_SYS_OK)
{
- status = TINY_STORE_IO_ERROR;
- goto fail;
+ return store_open_failed(store, lock_path, TINY_STORE_IO_ERROR);
}
{
uint64_t replay_offset = 0;
@@ -809,14 +757,10 @@ tiny_store_open(tiny_store_t **output, const wchar_t *directory)
}
if (status != TINY_STORE_OK)
{
- goto fail;
+ return store_open_failed(store, lock_path, status);
}
*output = store;
return TINY_STORE_OK;
-fail:
- free(lock_path);
- tiny_store_close(store);
- return status;
}
void
@@ -827,14 +771,8 @@ tiny_store_close(tiny_store_t *store)
{
return;
}
- if (store->log != INVALID_HANDLE_VALUE)
- {
- CloseHandle(store->log);
- }
- if (store->lock != INVALID_HANDLE_VALUE)
- {
- CloseHandle(store->lock);
- }
+ tiny_sys_file_close(&store->log);
+ tiny_sys_file_close(&store->lock);
for (index = 0; index < store->entry_count; ++index)
{
tiny_message_free(&store->entries[index].message);
@@ -873,6 +811,15 @@ tiny_store_write(tiny_store_t *store, const tiny_message_t *input, tiny_message_
unsigned char *payload = NULL;
uint32_t length = 0;
tiny_store_status_t status;
+ if (written != NULL)
+ {
+ tiny_message_init(written);
+ }
+ if (store == NULL || input == NULL || written == NULL || input->session_id == NULL
+ || input->content == NULL)
+ {
+ return TINY_STORE_IO_ERROR;
+ }
tiny_message_init(&message);
tiny_message_init(&indexed);
if (tiny_message_copy(&message, input) != 0)
@@ -890,7 +837,7 @@ tiny_store_write(tiny_store_t *store, const tiny_message_t *input, tiny_message_
return TINY_STORE_NO_MEMORY;
}
status
- = append_record_to(store->log, RECORD_PUT, store->last_sequence + 1, payload, length, true);
+ = append_record_to(&store->log, RECORD_PUT, store->last_sequence + 1, payload, length, true);
free(payload);
if (status != TINY_STORE_OK)
{
@@ -911,18 +858,23 @@ tiny_store_write(tiny_store_t *store, const tiny_message_t *input, tiny_message_
tiny_store_status_t
tiny_store_delete(tiny_store_t *store, const char *message_id, const char *session_id)
{
- tiny_store_entry_t *entry = find_entry(store, message_id);
+ tiny_store_entry_t *entry;
unsigned char payload[34];
tiny_store_status_t status;
+ if (store == NULL || message_id == NULL || !tiny_message_id_valid(message_id))
+ {
+ return TINY_STORE_NOT_FOUND;
+ }
+ entry = find_entry(store, message_id);
if (!entry || entry->deleted
|| (session_id && strcmp(entry->message.session_id, session_id) != 0))
{
return TINY_STORE_NOT_FOUND;
}
memcpy(payload, message_id, 26);
- put_u64(payload + 26, now_milliseconds());
+ tiny_codec_put_u64(payload + 26, now_milliseconds());
status = append_record_to(
- store->log, RECORD_DEL, store->last_sequence + 1, payload, sizeof(payload), true);
+ &store->log, RECORD_DEL, store->last_sequence + 1, payload, sizeof(payload), true);
if (status != TINY_STORE_OK)
{
set_error(store, "failed to append and flush a DEL record");
@@ -936,35 +888,45 @@ tiny_store_delete(tiny_store_t *store, const char *message_id, const char *sessi
tiny_store_status_t
tiny_store_rebuild(tiny_store_t *store, tiny_rebuild_report_t *report)
{
- wchar_t *temporary = join_path(store->directory, L"active.tmp");
- HANDLE file;
- LARGE_INTEGER before, after;
+ char *temporary;
+ tiny_sys_file_t file;
+ uint64_t before = 0, after = 0;
size_t read_index, write_index = 0;
uint64_t removed = 0, max_sequence = 0;
tiny_store_status_t status = TINY_STORE_OK;
+ unsigned char previous_generation[sizeof(store->generation)];
+ bool previous_has_generation;
+ bool log_replaced = false;
+ if (report != NULL)
+ {
+ memset(report, 0, sizeof(*report));
+ }
+ if (store == NULL || report == NULL)
+ {
+ return TINY_STORE_IO_ERROR;
+ }
+ memcpy(previous_generation, store->generation, sizeof(previous_generation));
+ previous_has_generation = store->has_generation;
+ temporary = tiny_sys_path_join(store->directory, "active.tmp");
if (!temporary)
{
return TINY_STORE_NO_MEMORY;
}
- GetFileSizeEx(store->log, &before);
- file = CreateFileW(
- temporary, GENERIC_WRITE | GENERIC_READ, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
- if (file == INVALID_HANDLE_VALUE)
+ if (!tiny_sys_file_size(&store->log, &before)
+ || tiny_sys_file_open(&file, temporary, TINY_SYS_FILE_WRITE_NEW) != TINY_SYS_OK)
{
free(temporary);
return TINY_STORE_IO_ERROR;
}
- if (BCryptGenRandom(
- NULL, store->generation, sizeof(store->generation), BCRYPT_USE_SYSTEM_PREFERRED_RNG)
- != 0)
+ if (!tiny_sys_random_bytes(store->generation, sizeof(store->generation)))
{
- CloseHandle(file);
+ tiny_sys_file_close(&file);
free(temporary);
return TINY_STORE_IO_ERROR;
}
store->has_generation = true;
status = append_record_to(
- file, RECORD_MARKER, 0, store->generation, sizeof(store->generation), false);
+ &file, RECORD_MARKER, 0, store->generation, sizeof(store->generation), false);
for (read_index = 0; read_index < store->entry_count; ++read_index)
{
if (status == TINY_STORE_OK && !store->entries[read_index].deleted)
@@ -977,7 +939,7 @@ tiny_store_rebuild(tiny_store_t *store, tiny_rebuild_report_t *report)
break;
}
status = append_record_to(
- file, RECORD_PUT, store->entries[read_index].sequence, payload, length, false);
+ &file, RECORD_PUT, store->entries[read_index].sequence, payload, length, false);
free(payload);
if (status != TINY_STORE_OK)
{
@@ -993,28 +955,24 @@ tiny_store_rebuild(tiny_store_t *store, tiny_rebuild_report_t *report)
removed++;
}
}
- if (status == TINY_STORE_OK && !FlushFileBuffers(file))
+ if (status == TINY_STORE_OK && !tiny_sys_file_flush(&file))
{
status = TINY_STORE_IO_ERROR;
}
- CloseHandle(file);
+ tiny_sys_file_close(&file);
if (status == TINY_STORE_OK)
{
- CloseHandle(store->log);
- store->log = INVALID_HANDLE_VALUE;
- if (!MoveFileExW(
- temporary, store->log_path, MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
+ tiny_sys_file_close(&store->log);
+ if (!tiny_sys_path_replace(temporary, store->log_path))
{
status = TINY_STORE_IO_ERROR;
}
- store->log = CreateFileW(store->log_path,
- GENERIC_READ | GENERIC_WRITE,
- FILE_SHARE_READ,
- NULL,
- OPEN_ALWAYS,
- FILE_ATTRIBUTE_NORMAL,
- NULL);
- if (store->log == INVALID_HANDLE_VALUE)
+ else
+ {
+ log_replaced = true;
+ }
+ if (tiny_sys_file_open(&store->log, store->log_path, TINY_SYS_FILE_READ_WRITE_CREATE)
+ != TINY_SYS_OK)
{
status = TINY_STORE_IO_ERROR;
}
@@ -1038,15 +996,20 @@ tiny_store_rebuild(tiny_store_t *store, tiny_rebuild_report_t *report)
}
store->entry_count = write_index;
store->last_sequence = max_sequence;
- GetFileSizeEx(store->log, &after);
+ (void)tiny_sys_file_size(&store->log, &after);
report->messages_indexed = write_index;
report->records_removed = removed;
- report->bytes_before = (uint64_t)before.QuadPart;
- report->bytes_after = (uint64_t)after.QuadPart;
+ report->bytes_before = before;
+ report->bytes_after = after;
status = write_snapshot(store);
}
if (status != TINY_STORE_OK)
{
+ if (!log_replaced)
+ {
+ memcpy(store->generation, previous_generation, sizeof(store->generation));
+ store->has_generation = previous_has_generation;
+ }
set_error(store,
"compaction did not complete; the previous valid log is retained when replacement "
"had not begun");
@@ -1058,28 +1021,30 @@ tiny_store_rebuild(tiny_store_t *store, tiny_rebuild_report_t *report)
void
tiny_store_health(const tiny_store_t *store, tiny_store_health_t *health)
{
- LARGE_INTEGER size;
+ uint64_t size;
size_t index;
+ if (health == NULL)
+ {
+ return;
+ }
memset(health, 0, sizeof(*health));
- if (GetFileSizeEx(store->log, &size))
+ if (store == NULL)
+ {
+ return;
+ }
+ if (tiny_sys_file_size(&store->log, &size))
{
- health->log_size = (uint64_t)size.QuadPart;
+ health->log_size = size;
}
{
- HANDLE f = CreateFileW(store->snapshot_path,
- GENERIC_READ,
- FILE_SHARE_READ,
- NULL,
- OPEN_EXISTING,
- FILE_ATTRIBUTE_NORMAL,
- NULL);
- if (f != INVALID_HANDLE_VALUE)
+ tiny_sys_file_t file;
+ if (tiny_sys_file_open(&file, store->snapshot_path, TINY_SYS_FILE_READ) == TINY_SYS_OK)
{
- if (GetFileSizeEx(f, &size))
+ if (tiny_sys_file_size(&file, &size))
{
- health->snapshot_size = (uint64_t)size.QuadPart;
+ health->snapshot_size = size;
}
- CloseHandle(f);
+ tiny_sys_file_close(&file);
}
}
for (index = 0; index < store->entry_count; ++index)
@@ -1096,16 +1061,16 @@ tiny_store_health(const tiny_store_t *store, tiny_store_health_t *health)
health->last_sequence = store->last_sequence;
health->recovered_tail = store->recovered_tail;
health->snapshot_valid = store->snapshot_valid;
- strncpy_s(health->last_error, sizeof(health->last_error), store->last_error, _TRUNCATE);
+ snprintf(health->last_error, sizeof(health->last_error), "%s", store->last_error);
}
size_t
tiny_store_entry_count(const tiny_store_t *store)
{
- return store->entry_count;
+ return store == NULL ? 0 : store->entry_count;
}
const tiny_store_entry_t *
tiny_store_entry(const tiny_store_t *store, size_t index)
{
- return index < store->entry_count ? &store->entries[index] : NULL;
+ return store != NULL && index < store->entry_count ? &store->entries[index] : NULL;
}
diff --git a/src/storage/store.h b/src/storage/store.h
index cc14a80..619f86f 100644
--- a/src/storage/store.h
+++ b/src/storage/store.h
@@ -4,7 +4,6 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
-#include <wchar.h>
#include "domain/message.h"
@@ -47,7 +46,7 @@ typedef struct tiny_store_health
char last_error[160];
} tiny_store_health_t;
-tiny_store_status_t tiny_store_open(tiny_store_t **store, const wchar_t *directory);
+tiny_store_status_t tiny_store_open(tiny_store_t **store, const char *directory);
void tiny_store_close(tiny_store_t *store);
const char *tiny_store_status_name(tiny_store_status_t status);
diff --git a/src/sys/README.md b/src/sys/README.md
new file mode 100644
index 0000000..22802d5
--- /dev/null
+++ b/src/sys/README.md
@@ -0,0 +1,21 @@
+# System abstraction
+
+`system.h` is the only OS-facing API used by the application and storage
+layers. Paths and process arguments are UTF-8 strings on every platform.
+
+The API owns these responsibilities:
+
+- process argument conversion and absolute/path-join operations;
+- directory creation, temporary directories, and path removal/replacement;
+- opaque file handles, locking, read/write/seek/truncate/flush operations;
+- wall-clock and monotonic milliseconds;
+- cryptographically secure random bytes.
+
+Returned strings are heap allocated and must be released with `free()`. Files
+must be initialized or opened through this API and closed with
+`tiny_sys_file_close()`.
+
+Platform selection is intentionally confined to the single conditional block
+in `system.c`. Windows uses wide Win32 calls internally; non-Windows builds use
+the POSIX implementation. No Win32 or POSIX type is exposed by `system.h`.
+
diff --git a/src/sys/encoding.c b/src/sys/encoding.c
new file mode 100644
index 0000000..ccdcc0a
--- /dev/null
+++ b/src/sys/encoding.c
@@ -0,0 +1,208 @@
+#include "sys/encoding.h"
+
+#include "base/allocator.h"
+
+#include <stdbool.h>
+#include <string.h>
+
+static bool
+utf8_codepoint(const unsigned char *input, size_t length, size_t *used, uint32_t *codepoint)
+{
+ const unsigned char first = input[0];
+ size_t width, index;
+ uint32_t value;
+ if (first < 0x80)
+ {
+ width = 1;
+ value = first;
+ }
+ else if (first >= 0xc2 && first <= 0xdf)
+ {
+ width = 2;
+ value = first & 0x1f;
+ }
+ else if (first >= 0xe0 && first <= 0xef)
+ {
+ width = 3;
+ value = first & 0x0f;
+ }
+ else if (first >= 0xf0 && first <= 0xf4)
+ {
+ width = 4;
+ value = first & 0x07;
+ }
+ else
+ {
+ return false;
+ }
+ if (width > length)
+ {
+ return false;
+ }
+ for (index = 1; index < width; ++index)
+ {
+ if ((input[index] & 0xc0) != 0x80)
+ {
+ return false;
+ }
+ value = (value << 6) | (input[index] & 0x3f);
+ }
+ if ((width == 3 && ((first == 0xe0 && input[1] < 0xa0) || (first == 0xed && input[1] >= 0xa0)))
+ || (width == 4
+ && ((first == 0xf0 && input[1] < 0x90) || (first == 0xf4 && input[1] >= 0x90))))
+ {
+ return false;
+ }
+ *used = width;
+ *codepoint = value;
+ return true;
+}
+
+tiny_encoding_status_t
+tiny_utf8_to_utf16(const char *input, size_t input_length, uint16_t **output, size_t *output_length)
+{
+ size_t offset = 0, units = 0;
+ uint16_t *result;
+ if (output == NULL || output_length == NULL)
+ {
+ return TINY_ENCODING_INVALID;
+ }
+ *output = NULL;
+ *output_length = 0;
+ if (input == NULL && input_length != 0)
+ {
+ return TINY_ENCODING_INVALID;
+ }
+ while (offset < input_length)
+ {
+ size_t used;
+ uint32_t point;
+ if (!utf8_codepoint(
+ (const unsigned char *)input + offset, input_length - offset, &used, &point))
+ {
+ return TINY_ENCODING_INVALID;
+ }
+ if (units > SIZE_MAX - (point >= 0x10000 ? 2U : 1U))
+ {
+ return TINY_ENCODING_OVERFLOW;
+ }
+ units += point >= 0x10000 ? 2U : 1U;
+ offset += used;
+ }
+ if (units == SIZE_MAX || units + 1 > SIZE_MAX / sizeof(*result))
+ {
+ return TINY_ENCODING_OVERFLOW;
+ }
+ result = malloc((units + 1) * sizeof(*result));
+ if (result == NULL)
+ {
+ return TINY_ENCODING_NO_MEMORY;
+ }
+ offset = 0;
+ units = 0;
+ while (offset < input_length)
+ {
+ size_t used;
+ uint32_t point;
+ (void)utf8_codepoint(
+ (const unsigned char *)input + offset, input_length - offset, &used, &point);
+ if (point < 0x10000)
+ {
+ result[units++] = (uint16_t)point;
+ }
+ else
+ {
+ point -= 0x10000;
+ result[units++] = (uint16_t)(0xd800 + (point >> 10));
+ result[units++] = (uint16_t)(0xdc00 + (point & 0x3ff));
+ }
+ offset += used;
+ }
+ result[units] = 0;
+ *output = result;
+ *output_length = units;
+ return TINY_ENCODING_OK;
+}
+
+tiny_encoding_status_t
+tiny_utf16_to_utf8(const uint16_t *input, size_t input_length, char **output, size_t *output_length)
+{
+ size_t index = 0, bytes = 0;
+ char *result;
+ if (output == NULL || output_length == NULL)
+ {
+ return TINY_ENCODING_INVALID;
+ }
+ *output = NULL;
+ *output_length = 0;
+ if (input == NULL && input_length != 0)
+ {
+ return TINY_ENCODING_INVALID;
+ }
+ while (index < input_length)
+ {
+ uint32_t point = input[index++];
+ if (point >= 0xd800 && point <= 0xdbff)
+ {
+ if (index >= input_length || input[index] < 0xdc00 || input[index] > 0xdfff)
+ {
+ return TINY_ENCODING_INVALID;
+ }
+ point = 0x10000 + ((point - 0xd800) << 10) + (input[index++] - 0xdc00);
+ }
+ else if (point >= 0xdc00 && point <= 0xdfff)
+ {
+ return TINY_ENCODING_INVALID;
+ }
+ if (bytes > SIZE_MAX - (point < 0x80 ? 1U : point < 0x800 ? 2U : point < 0x10000 ? 3U : 4U))
+ {
+ return TINY_ENCODING_OVERFLOW;
+ }
+ bytes += point < 0x80 ? 1U : point < 0x800 ? 2U : point < 0x10000 ? 3U : 4U;
+ }
+ if (bytes == SIZE_MAX)
+ {
+ return TINY_ENCODING_OVERFLOW;
+ }
+ result = malloc(bytes + 1);
+ if (result == NULL)
+ {
+ return TINY_ENCODING_NO_MEMORY;
+ }
+ index = 0;
+ bytes = 0;
+ while (index < input_length)
+ {
+ uint32_t point = input[index++];
+ if (point >= 0xd800 && point <= 0xdbff)
+ {
+ point = 0x10000 + ((point - 0xd800) << 10) + (input[index++] - 0xdc00);
+ }
+ if (point < 0x80)
+ {
+ result[bytes++] = (char)point;
+ }
+ else if (point < 0x800)
+ {
+ result[bytes++] = (char)(0xc0 | (point >> 6));
+ result[bytes++] = (char)(0x80 | (point & 0x3f));
+ }
+ else if (point < 0x10000)
+ {
+ result[bytes++] = (char)(0xe0 | (point >> 12));
+ result[bytes++] = (char)(0x80 | ((point >> 6) & 0x3f));
+ result[bytes++] = (char)(0x80 | (point & 0x3f));
+ }
+ else
+ {
+ result[bytes++] = (char)(0xf0 | (point >> 18));
+ result[bytes++] = (char)(0x80 | ((point >> 12) & 0x3f));
+ result[bytes++] = (char)(0x80 | ((point >> 6) & 0x3f));
+ result[bytes++] = (char)(0x80 | (point & 0x3f));
+ }
+ }
+ result[bytes] = '\0';
+ *output = result;
+ *output_length = bytes;
+ return TINY_ENCODING_OK;
+}
diff --git a/src/sys/encoding.h b/src/sys/encoding.h
new file mode 100644
index 0000000..386d319
--- /dev/null
+++ b/src/sys/encoding.h
@@ -0,0 +1,24 @@
+#ifndef TINY_CONVMEM_SYS_ENCODING_H
+#define TINY_CONVMEM_SYS_ENCODING_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+typedef enum tiny_encoding_status
+{
+ TINY_ENCODING_OK = 0,
+ TINY_ENCODING_INVALID,
+ TINY_ENCODING_NO_MEMORY,
+ TINY_ENCODING_OVERFLOW
+} tiny_encoding_status_t;
+
+tiny_encoding_status_t tiny_utf8_to_utf16(const char *input,
+ size_t input_length,
+ uint16_t **output,
+ size_t *output_length);
+tiny_encoding_status_t tiny_utf16_to_utf8(const uint16_t *input,
+ size_t input_length,
+ char **output,
+ size_t *output_length);
+
+#endif
diff --git a/src/sys/system.c b/src/sys/system.c
new file mode 100644
index 0000000..63d8297
--- /dev/null
+++ b/src/sys/system.c
@@ -0,0 +1,890 @@
+#include "sys/system.h"
+#include "sys/encoding.h"
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "base/allocator.h"
+
+typedef struct tiny_sys_test_state
+{
+ size_t calls[TINY_SYS_TEST_OPERATION_COUNT];
+ size_t failures[TINY_SYS_TEST_OPERATION_COUNT];
+ uint32_t max_transfer;
+} tiny_sys_test_state_t;
+
+static tiny_sys_test_state_t tiny_sys_test_state;
+
+void
+tiny_sys_test_reset(void)
+{
+ memset(&tiny_sys_test_state, 0, sizeof(tiny_sys_test_state));
+}
+
+bool
+tiny_sys_test_fail_after(tiny_sys_test_operation_t operation, size_t call_index)
+{
+ if (operation < TINY_SYS_TEST_OPEN || operation >= TINY_SYS_TEST_OPERATION_COUNT
+ || call_index == 0)
+ {
+ return false;
+ }
+ tiny_sys_test_state.calls[operation] = 0;
+ tiny_sys_test_state.failures[operation] = call_index;
+ return true;
+}
+
+void
+tiny_sys_test_max_transfer(uint32_t length)
+{
+ tiny_sys_test_state.max_transfer = length;
+}
+
+static bool
+tiny_sys_test_should_fail(tiny_sys_test_operation_t operation)
+{
+ ++tiny_sys_test_state.calls[operation];
+ return tiny_sys_test_state.failures[operation] != 0
+ && tiny_sys_test_state.calls[operation] == tiny_sys_test_state.failures[operation];
+}
+
+static uint32_t
+tiny_sys_test_transfer_length(uint32_t length)
+{
+ return tiny_sys_test_state.max_transfer != 0 && length > tiny_sys_test_state.max_transfer
+ ? tiny_sys_test_state.max_transfer
+ : length;
+}
+
+#if defined(_WIN32)
+
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#include <bcrypt.h>
+#include <shellapi.h>
+
+static wchar_t *
+utf8_to_wide(const char *value)
+{
+ uint16_t *result;
+ size_t length;
+ _Static_assert(sizeof(wchar_t) == sizeof(uint16_t), "Windows wchar_t must be UTF-16");
+ if (value == NULL)
+ {
+ return NULL;
+ }
+ if (tiny_utf8_to_utf16(value, strlen(value), &result, &length) != TINY_ENCODING_OK)
+ {
+ return NULL;
+ }
+ return (wchar_t *)result;
+}
+
+static char *
+wide_to_utf8(const wchar_t *value)
+{
+ char *result;
+ size_t length;
+ if (value == NULL)
+ {
+ return NULL;
+ }
+ if (tiny_utf16_to_utf8((const uint16_t *)value, wcslen(value), &result, &length)
+ != TINY_ENCODING_OK)
+ {
+ return NULL;
+ }
+ return result;
+}
+
+static HANDLE
+native_file(const tiny_sys_file_t *file)
+{
+ return (HANDLE)file->native;
+}
+
+bool
+tiny_sys_arguments_init(tiny_sys_arguments_t *arguments, int argc, char **argv)
+{
+ wchar_t **wide_arguments;
+ int wide_count, index;
+ (void)argc;
+ (void)argv;
+ if (arguments == NULL)
+ {
+ return false;
+ }
+ memset(arguments, 0, sizeof(*arguments));
+ wide_arguments = CommandLineToArgvW(GetCommandLineW(), &wide_count);
+ if (wide_arguments == NULL)
+ {
+ return false;
+ }
+ arguments->values = calloc((size_t)wide_count, sizeof(*arguments->values));
+ if (arguments->values == NULL)
+ {
+ LocalFree(wide_arguments);
+ return false;
+ }
+ arguments->count = wide_count;
+ for (index = 0; index < wide_count; ++index)
+ {
+ arguments->values[index] = wide_to_utf8(wide_arguments[index]);
+ if (arguments->values[index] == NULL)
+ {
+ LocalFree(wide_arguments);
+ tiny_sys_arguments_free(arguments);
+ return false;
+ }
+ }
+ LocalFree(wide_arguments);
+ return true;
+}
+
+char *
+tiny_sys_path_absolute(const char *path)
+{
+ wchar_t *wide_path = utf8_to_wide(path);
+ wchar_t *wide_result;
+ char *result;
+ DWORD required;
+ if (wide_path == NULL)
+ {
+ return NULL;
+ }
+ required = GetFullPathNameW(wide_path, 0, NULL, NULL);
+ if (required == 0)
+ {
+ free(wide_path);
+ return NULL;
+ }
+ wide_result = malloc((size_t)required * sizeof(*wide_result));
+ if (wide_result == NULL || GetFullPathNameW(wide_path, required, wide_result, NULL) == 0)
+ {
+ free(wide_path);
+ free(wide_result);
+ return NULL;
+ }
+ result = wide_to_utf8(wide_result);
+ free(wide_path);
+ free(wide_result);
+ return result;
+}
+
+char *
+tiny_sys_path_join(const char *directory, const char *name)
+{
+ size_t left, right, required;
+ if (directory == NULL || name == NULL)
+ {
+ return NULL;
+ }
+ left = strlen(directory);
+ right = strlen(name);
+ const bool separator = left > 0 && directory[left - 1] != '\\' && directory[left - 1] != '/';
+ if (left > SIZE_MAX - right - (separator ? 2u : 1u))
+ {
+ return NULL;
+ }
+ required = left + right + (separator ? 2u : 1u);
+ char *result = malloc(required);
+ if (result == NULL)
+ {
+ return NULL;
+ }
+ memcpy(result, directory, left);
+ if (separator)
+ {
+ result[left] = '\\';
+ }
+ memcpy(result + left + (separator ? 1 : 0), name, right + 1);
+ return result;
+}
+
+tiny_sys_status_t
+tiny_sys_directory_create(const char *path)
+{
+ wchar_t *wide_path = utf8_to_wide(path);
+ DWORD error;
+ if (wide_path == NULL)
+ {
+ return TINY_SYS_ERROR;
+ }
+ if (CreateDirectoryW(wide_path, NULL))
+ {
+ free(wide_path);
+ return TINY_SYS_OK;
+ }
+ error = GetLastError();
+ free(wide_path);
+ return error == ERROR_ALREADY_EXISTS ? TINY_SYS_OK : TINY_SYS_ERROR;
+}
+
+tiny_sys_status_t
+tiny_sys_file_open(tiny_sys_file_t *file, const char *path, tiny_sys_file_mode_t mode)
+{
+ wchar_t *wide_path = utf8_to_wide(path);
+ DWORD access = 0, share = 0, creation = OPEN_EXISTING, error;
+ HANDLE handle;
+ if (file == NULL)
+ {
+ free(wide_path);
+ return TINY_SYS_ERROR;
+ }
+ tiny_sys_file_init(file);
+ if (wide_path == NULL || tiny_sys_test_should_fail(TINY_SYS_TEST_OPEN))
+ {
+ free(wide_path);
+ return TINY_SYS_ERROR;
+ }
+ switch (mode)
+ {
+ case TINY_SYS_FILE_READ:
+ access = GENERIC_READ;
+ share = FILE_SHARE_READ;
+ break;
+ case TINY_SYS_FILE_WRITE_NEW:
+ access = GENERIC_WRITE;
+ creation = CREATE_ALWAYS;
+ break;
+ case TINY_SYS_FILE_READ_WRITE:
+ access = GENERIC_READ | GENERIC_WRITE;
+ share = FILE_SHARE_READ;
+ break;
+ case TINY_SYS_FILE_READ_WRITE_CREATE:
+ access = GENERIC_READ | GENERIC_WRITE;
+ share = FILE_SHARE_READ;
+ creation = OPEN_ALWAYS;
+ break;
+ case TINY_SYS_FILE_LOCK:
+ access = GENERIC_READ | GENERIC_WRITE;
+ creation = OPEN_ALWAYS;
+ break;
+ default:
+ free(wide_path);
+ return TINY_SYS_ERROR;
+ }
+ handle = CreateFileW(wide_path, access, share, NULL, creation, FILE_ATTRIBUTE_NORMAL, NULL);
+ free(wide_path);
+ if (handle != INVALID_HANDLE_VALUE)
+ {
+ file->native = (intptr_t)handle;
+ return TINY_SYS_OK;
+ }
+ error = GetLastError();
+ if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND)
+ {
+ return TINY_SYS_NOT_FOUND;
+ }
+ if (error == ERROR_SHARING_VIOLATION || error == ERROR_LOCK_VIOLATION)
+ {
+ return TINY_SYS_BUSY;
+ }
+ return TINY_SYS_ERROR;
+}
+
+void
+tiny_sys_file_init(tiny_sys_file_t *file)
+{
+ if (file != NULL)
+ {
+ file->native = 0;
+ }
+}
+
+bool
+tiny_sys_file_valid(const tiny_sys_file_t *file)
+{
+ return file != NULL && file->native != 0;
+}
+
+void
+tiny_sys_file_close(tiny_sys_file_t *file)
+{
+ if (tiny_sys_file_valid(file))
+ {
+ CloseHandle(native_file(file));
+ tiny_sys_file_init(file);
+ }
+}
+
+bool
+tiny_sys_file_read(tiny_sys_file_t *file, void *data, uint32_t length, uint32_t *actual)
+{
+ DWORD count = 0;
+ bool result;
+ if (actual == NULL)
+ {
+ return false;
+ }
+ *actual = 0;
+ if (!tiny_sys_file_valid(file) || (data == NULL && length != 0)
+ || tiny_sys_test_should_fail(TINY_SYS_TEST_READ))
+ {
+ return false;
+ }
+ length = tiny_sys_test_transfer_length(length);
+ result = ReadFile(native_file(file), data, length, &count, NULL) != 0;
+ *actual = count;
+ return result;
+}
+
+bool
+tiny_sys_file_write(tiny_sys_file_t *file, const void *data, uint32_t length, uint32_t *actual)
+{
+ DWORD count = 0;
+ bool result;
+ if (actual == NULL)
+ {
+ return false;
+ }
+ *actual = 0;
+ if (!tiny_sys_file_valid(file) || (data == NULL && length != 0)
+ || tiny_sys_test_should_fail(TINY_SYS_TEST_WRITE))
+ {
+ return false;
+ }
+ length = tiny_sys_test_transfer_length(length);
+ result = WriteFile(native_file(file), data, length, &count, NULL) != 0;
+ *actual = count;
+ return result;
+}
+
+bool
+tiny_sys_file_seek(tiny_sys_file_t *file, int64_t offset, tiny_sys_seek_origin_t origin)
+{
+ LARGE_INTEGER distance;
+ DWORD method;
+ if (!tiny_sys_file_valid(file) || tiny_sys_test_should_fail(TINY_SYS_TEST_SEEK))
+ {
+ return false;
+ }
+ distance.QuadPart = offset;
+ switch (origin)
+ {
+ case TINY_SYS_SEEK_BEGIN:
+ method = FILE_BEGIN;
+ break;
+ case TINY_SYS_SEEK_CURRENT:
+ method = FILE_CURRENT;
+ break;
+ case TINY_SYS_SEEK_END:
+ method = FILE_END;
+ break;
+ default:
+ return false;
+ }
+ return SetFilePointerEx(native_file(file), distance, NULL, method) != 0;
+}
+
+bool
+tiny_sys_file_size(const tiny_sys_file_t *file, uint64_t *size)
+{
+ LARGE_INTEGER native_size;
+ if (size == NULL)
+ {
+ return false;
+ }
+ *size = 0;
+ if (!tiny_sys_file_valid(file) || tiny_sys_test_should_fail(TINY_SYS_TEST_SIZE)
+ || !GetFileSizeEx(native_file(file), &native_size))
+ {
+ return false;
+ }
+ *size = (uint64_t)native_size.QuadPart;
+ return true;
+}
+
+bool
+tiny_sys_file_truncate(tiny_sys_file_t *file)
+{
+ return tiny_sys_file_valid(file) && !tiny_sys_test_should_fail(TINY_SYS_TEST_TRUNCATE)
+ && SetEndOfFile(native_file(file)) != 0;
+}
+
+bool
+tiny_sys_file_flush(tiny_sys_file_t *file)
+{
+ return tiny_sys_file_valid(file) && !tiny_sys_test_should_fail(TINY_SYS_TEST_FLUSH)
+ && FlushFileBuffers(native_file(file)) != 0;
+}
+
+bool
+tiny_sys_path_replace(const char *source, const char *destination)
+{
+ wchar_t *wide_source = utf8_to_wide(source);
+ wchar_t *wide_destination = utf8_to_wide(destination);
+ bool result = false;
+ if (wide_source != NULL && wide_destination != NULL
+ && !tiny_sys_test_should_fail(TINY_SYS_TEST_REPLACE))
+ {
+ result = MoveFileExW(wide_source,
+ wide_destination,
+ MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)
+ != 0;
+ }
+ free(wide_source);
+ free(wide_destination);
+ return result;
+}
+
+bool
+tiny_sys_file_remove(const char *path)
+{
+ wchar_t *wide_path = utf8_to_wide(path);
+ bool result = wide_path != NULL && DeleteFileW(wide_path) != 0;
+ free(wide_path);
+ return result;
+}
+
+bool
+tiny_sys_directory_remove(const char *path)
+{
+ wchar_t *wide_path = utf8_to_wide(path);
+ bool result = wide_path != NULL && RemoveDirectoryW(wide_path) != 0;
+ free(wide_path);
+ return result;
+}
+
+char *
+tiny_sys_temp_directory_create(const char *prefix)
+{
+ wchar_t temporary[MAX_PATH], candidate[MAX_PATH], wide_prefix[4] = L"tcm";
+ char *result;
+ size_t index;
+ if (prefix == NULL)
+ {
+ return NULL;
+ }
+ for (index = 0; index < 3 && prefix[index] != '\0'; ++index)
+ {
+ wide_prefix[index] = (wchar_t)(unsigned char)prefix[index];
+ }
+ if (GetTempPathW(MAX_PATH, temporary) == 0
+ || GetTempFileNameW(temporary, wide_prefix, 0, candidate) == 0 || !DeleteFileW(candidate)
+ || !CreateDirectoryW(candidate, NULL))
+ {
+ return NULL;
+ }
+ result = wide_to_utf8(candidate);
+ if (result == NULL)
+ {
+ RemoveDirectoryW(candidate);
+ }
+ return result;
+}
+
+uint64_t
+tiny_sys_now_milliseconds(void)
+{
+ FILETIME time;
+ ULARGE_INTEGER ticks;
+ GetSystemTimeAsFileTime(&time);
+ ticks.LowPart = time.dwLowDateTime;
+ ticks.HighPart = time.dwHighDateTime;
+ return ticks.QuadPart / 10000ULL - 11644473600000ULL;
+}
+
+uint64_t
+tiny_sys_monotonic_milliseconds(void)
+{
+ return GetTickCount64();
+}
+
+bool
+tiny_sys_random_bytes(void *data, size_t length)
+{
+ return (data != NULL || length == 0) && !tiny_sys_test_should_fail(TINY_SYS_TEST_RANDOM)
+ && length <= ULONG_MAX
+ && BCryptGenRandom(NULL, data, (ULONG)length, BCRYPT_USE_SYSTEM_PREFERRED_RNG) == 0;
+}
+
+#else
+
+#include <fcntl.h>
+#include <sys/file.h>
+#include <sys/stat.h>
+#include <time.h>
+#include <unistd.h>
+
+static char *
+duplicate_string(const char *value)
+{
+ const size_t length = strlen(value);
+ char *copy = malloc(length + 1);
+ if (copy != NULL)
+ {
+ memcpy(copy, value, length + 1);
+ }
+ return copy;
+}
+
+static int
+native_file(const tiny_sys_file_t *file)
+{
+ return (int)file->native;
+}
+
+bool
+tiny_sys_arguments_init(tiny_sys_arguments_t *arguments, int argc, char **argv)
+{
+ int index;
+ if (arguments == NULL || argc < 0 || (argc != 0 && argv == NULL))
+ {
+ return false;
+ }
+ memset(arguments, 0, sizeof(*arguments));
+ arguments->values = calloc((size_t)argc, sizeof(*arguments->values));
+ if (arguments->values == NULL)
+ {
+ return false;
+ }
+ arguments->count = argc;
+ for (index = 0; index < argc; ++index)
+ {
+ arguments->values[index] = duplicate_string(argv[index]);
+ if (arguments->values[index] == NULL)
+ {
+ tiny_sys_arguments_free(arguments);
+ return false;
+ }
+ }
+ return true;
+}
+
+char *
+tiny_sys_path_join(const char *directory, const char *name)
+{
+ size_t left, right, required;
+ if (directory == NULL || name == NULL)
+ {
+ return NULL;
+ }
+ left = strlen(directory);
+ right = strlen(name);
+ const bool separator = left > 0 && directory[left - 1] != '/';
+ if (left > SIZE_MAX - right - (separator ? 2u : 1u))
+ {
+ return NULL;
+ }
+ required = left + right + (separator ? 2u : 1u);
+ char *result = malloc(required);
+ if (result == NULL)
+ {
+ return NULL;
+ }
+ memcpy(result, directory, left);
+ if (separator)
+ {
+ result[left] = '/';
+ }
+ memcpy(result + left + (separator ? 1 : 0), name, right + 1);
+ return result;
+}
+
+char *
+tiny_sys_path_absolute(const char *path)
+{
+ char *working_directory, *result;
+ if (path == NULL)
+ {
+ return NULL;
+ }
+ if (path[0] == '/')
+ {
+ return duplicate_string(path);
+ }
+ working_directory = getcwd(NULL, 0);
+ if (working_directory == NULL)
+ {
+ return NULL;
+ }
+ result = tiny_sys_path_join(working_directory, path);
+ free(working_directory);
+ return result;
+}
+
+tiny_sys_status_t
+tiny_sys_directory_create(const char *path)
+{
+ return mkdir(path, 0700) == 0 || errno == EEXIST ? TINY_SYS_OK : TINY_SYS_ERROR;
+}
+
+void
+tiny_sys_file_init(tiny_sys_file_t *file)
+{
+ if (file != NULL)
+ {
+ file->native = -1;
+ }
+}
+
+bool
+tiny_sys_file_valid(const tiny_sys_file_t *file)
+{
+ return file != NULL && file->native >= 0;
+}
+
+tiny_sys_status_t
+tiny_sys_file_open(tiny_sys_file_t *file, const char *path, tiny_sys_file_mode_t mode)
+{
+ int flags, descriptor;
+ if (file == NULL)
+ {
+ return TINY_SYS_ERROR;
+ }
+ tiny_sys_file_init(file);
+ if (path == NULL || tiny_sys_test_should_fail(TINY_SYS_TEST_OPEN))
+ {
+ return TINY_SYS_ERROR;
+ }
+ switch (mode)
+ {
+ case TINY_SYS_FILE_READ:
+ flags = O_RDONLY;
+ break;
+ case TINY_SYS_FILE_WRITE_NEW:
+ flags = O_WRONLY | O_CREAT | O_TRUNC;
+ break;
+ case TINY_SYS_FILE_READ_WRITE:
+ flags = O_RDWR;
+ break;
+ case TINY_SYS_FILE_READ_WRITE_CREATE:
+ case TINY_SYS_FILE_LOCK:
+ flags = O_RDWR | O_CREAT;
+ break;
+ default:
+ return TINY_SYS_ERROR;
+ }
+ descriptor = open(path, flags, 0600);
+ if (descriptor < 0)
+ {
+ return errno == ENOENT ? TINY_SYS_NOT_FOUND : TINY_SYS_ERROR;
+ }
+ if (mode == TINY_SYS_FILE_LOCK && flock(descriptor, LOCK_EX | LOCK_NB) != 0)
+ {
+ close(descriptor);
+ return errno == EWOULDBLOCK ? TINY_SYS_BUSY : TINY_SYS_ERROR;
+ }
+ file->native = descriptor;
+ return TINY_SYS_OK;
+}
+
+void
+tiny_sys_file_close(tiny_sys_file_t *file)
+{
+ if (tiny_sys_file_valid(file))
+ {
+ close(native_file(file));
+ tiny_sys_file_init(file);
+ }
+}
+
+bool
+tiny_sys_file_read(tiny_sys_file_t *file, void *data, uint32_t length, uint32_t *actual)
+{
+ ssize_t count;
+ if (actual == NULL)
+ {
+ return false;
+ }
+ *actual = 0;
+ if (!tiny_sys_file_valid(file) || (data == NULL && length != 0)
+ || tiny_sys_test_should_fail(TINY_SYS_TEST_READ))
+ {
+ return false;
+ }
+ length = tiny_sys_test_transfer_length(length);
+ do
+ {
+ count = read(native_file(file), data, length);
+ }
+ while (count < 0 && errno == EINTR);
+ *actual = count > 0 ? (uint32_t)count : 0;
+ return count >= 0;
+}
+
+bool
+tiny_sys_file_write(tiny_sys_file_t *file, const void *data, uint32_t length, uint32_t *actual)
+{
+ ssize_t count;
+ if (actual == NULL)
+ {
+ return false;
+ }
+ *actual = 0;
+ if (!tiny_sys_file_valid(file) || (data == NULL && length != 0)
+ || tiny_sys_test_should_fail(TINY_SYS_TEST_WRITE))
+ {
+ return false;
+ }
+ length = tiny_sys_test_transfer_length(length);
+ do
+ {
+ count = write(native_file(file), data, length);
+ }
+ while (count < 0 && errno == EINTR);
+ *actual = count > 0 ? (uint32_t)count : 0;
+ return count >= 0;
+}
+
+bool
+tiny_sys_file_seek(tiny_sys_file_t *file, int64_t offset, tiny_sys_seek_origin_t origin)
+{
+ static const int origins[] = { SEEK_SET, SEEK_CUR, SEEK_END };
+ if (!tiny_sys_file_valid(file) || origin < TINY_SYS_SEEK_BEGIN || origin > TINY_SYS_SEEK_END
+ || tiny_sys_test_should_fail(TINY_SYS_TEST_SEEK))
+ {
+ return false;
+ }
+ return lseek(native_file(file), (off_t)offset, origins[origin]) >= 0;
+}
+
+bool
+tiny_sys_file_size(const tiny_sys_file_t *file, uint64_t *size)
+{
+ struct stat information;
+ if (size == NULL)
+ {
+ return false;
+ }
+ *size = 0;
+ if (!tiny_sys_file_valid(file) || tiny_sys_test_should_fail(TINY_SYS_TEST_SIZE)
+ || fstat(native_file(file), &information) != 0)
+ {
+ return false;
+ }
+ *size = (uint64_t)information.st_size;
+ return true;
+}
+
+bool
+tiny_sys_file_truncate(tiny_sys_file_t *file)
+{
+ off_t position;
+ if (!tiny_sys_file_valid(file) || tiny_sys_test_should_fail(TINY_SYS_TEST_TRUNCATE))
+ {
+ return false;
+ }
+ position = lseek(native_file(file), 0, SEEK_CUR);
+ return position >= 0 && ftruncate(native_file(file), position) == 0;
+}
+
+bool
+tiny_sys_file_flush(tiny_sys_file_t *file)
+{
+ return tiny_sys_file_valid(file) && !tiny_sys_test_should_fail(TINY_SYS_TEST_FLUSH)
+ && fsync(native_file(file)) == 0;
+}
+
+bool
+tiny_sys_path_replace(const char *source, const char *destination)
+{
+ return source != NULL && destination != NULL && !tiny_sys_test_should_fail(TINY_SYS_TEST_REPLACE)
+ && rename(source, destination) == 0;
+}
+
+bool
+tiny_sys_file_remove(const char *path)
+{
+ return unlink(path) == 0;
+}
+
+bool
+tiny_sys_directory_remove(const char *path)
+{
+ return rmdir(path) == 0;
+}
+
+char *
+tiny_sys_temp_directory_create(const char *prefix)
+{
+ const char *base = getenv("TMPDIR");
+ char *pattern, *result;
+ size_t length;
+ if (prefix == NULL)
+ {
+ return NULL;
+ }
+ if (base == NULL)
+ {
+ base = "/tmp";
+ }
+ length = strlen(base) + strlen(prefix) + 9;
+ pattern = malloc(length);
+ if (pattern == NULL)
+ {
+ return NULL;
+ }
+ snprintf(pattern, length, "%s/%sXXXXXX", base, prefix);
+ result = mkdtemp(pattern);
+ if (result == NULL)
+ {
+ free(pattern);
+ return NULL;
+ }
+ return pattern;
+}
+
+uint64_t
+tiny_sys_now_milliseconds(void)
+{
+ struct timespec time;
+ clock_gettime(CLOCK_REALTIME, &time);
+ return (uint64_t)time.tv_sec * 1000ULL + (uint64_t)time.tv_nsec / 1000000ULL;
+}
+
+uint64_t
+tiny_sys_monotonic_milliseconds(void)
+{
+ struct timespec time;
+ clock_gettime(CLOCK_MONOTONIC, &time);
+ return (uint64_t)time.tv_sec * 1000ULL + (uint64_t)time.tv_nsec / 1000000ULL;
+}
+
+bool
+tiny_sys_random_bytes(void *data, size_t length)
+{
+ tiny_sys_file_t file;
+ unsigned char *position = data;
+ size_t total = 0;
+ if ((data == NULL && length != 0) || tiny_sys_test_should_fail(TINY_SYS_TEST_RANDOM)
+ || tiny_sys_file_open(&file, "/dev/urandom", TINY_SYS_FILE_READ) != TINY_SYS_OK)
+ {
+ return false;
+ }
+ while (total < length)
+ {
+ uint32_t actual = 0;
+ const uint32_t requested
+ = length - total > UINT32_MAX ? UINT32_MAX : (uint32_t)(length - total);
+ if (!tiny_sys_file_read(&file, position + total, requested, &actual) || actual == 0)
+ {
+ tiny_sys_file_close(&file);
+ return false;
+ }
+ total += actual;
+ }
+ tiny_sys_file_close(&file);
+ return true;
+}
+
+#endif
+
+void
+tiny_sys_arguments_free(tiny_sys_arguments_t *arguments)
+{
+ int index;
+ if (arguments == NULL)
+ {
+ return;
+ }
+ for (index = 0; index < arguments->count; ++index)
+ {
+ free(arguments->values[index]);
+ }
+ free(arguments->values);
+ memset(arguments, 0, sizeof(*arguments));
+}
diff --git a/src/sys/system.h b/src/sys/system.h
new file mode 100644
index 0000000..b526ec5
--- /dev/null
+++ b/src/sys/system.h
@@ -0,0 +1,90 @@
+#ifndef TINY_CONVMEM_SYS_SYSTEM_H
+#define TINY_CONVMEM_SYS_SYSTEM_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+typedef enum tiny_sys_status
+{
+ TINY_SYS_OK = 0,
+ TINY_SYS_NOT_FOUND,
+ TINY_SYS_BUSY,
+ TINY_SYS_ERROR
+} tiny_sys_status_t;
+
+typedef enum tiny_sys_file_mode
+{
+ TINY_SYS_FILE_READ = 0,
+ TINY_SYS_FILE_WRITE_NEW,
+ TINY_SYS_FILE_READ_WRITE,
+ TINY_SYS_FILE_READ_WRITE_CREATE,
+ TINY_SYS_FILE_LOCK
+} tiny_sys_file_mode_t;
+
+typedef enum tiny_sys_seek_origin
+{
+ TINY_SYS_SEEK_BEGIN = 0,
+ TINY_SYS_SEEK_CURRENT,
+ TINY_SYS_SEEK_END
+} tiny_sys_seek_origin_t;
+
+typedef struct tiny_sys_file
+{
+ intptr_t native;
+} tiny_sys_file_t;
+
+typedef struct tiny_sys_arguments
+{
+ int count;
+ char **values;
+} tiny_sys_arguments_t;
+
+/* Internal test seam. A call index of 1 fails the next matching operation. */
+typedef enum tiny_sys_test_operation
+{
+ TINY_SYS_TEST_OPEN = 0,
+ TINY_SYS_TEST_READ,
+ TINY_SYS_TEST_WRITE,
+ TINY_SYS_TEST_SEEK,
+ TINY_SYS_TEST_SIZE,
+ TINY_SYS_TEST_TRUNCATE,
+ TINY_SYS_TEST_FLUSH,
+ TINY_SYS_TEST_REPLACE,
+ TINY_SYS_TEST_RANDOM,
+ TINY_SYS_TEST_OPERATION_COUNT
+} tiny_sys_test_operation_t;
+
+void tiny_sys_test_reset(void);
+bool tiny_sys_test_fail_after(tiny_sys_test_operation_t operation, size_t call_index);
+void tiny_sys_test_max_transfer(uint32_t length);
+
+bool tiny_sys_arguments_init(tiny_sys_arguments_t *arguments, int argc, char **argv);
+void tiny_sys_arguments_free(tiny_sys_arguments_t *arguments);
+
+char *tiny_sys_path_absolute(const char *path);
+char *tiny_sys_path_join(const char *directory, const char *name);
+char *tiny_sys_temp_directory_create(const char *prefix);
+tiny_sys_status_t tiny_sys_directory_create(const char *path);
+bool tiny_sys_file_remove(const char *path);
+bool tiny_sys_directory_remove(const char *path);
+bool tiny_sys_path_replace(const char *source, const char *destination);
+
+void tiny_sys_file_init(tiny_sys_file_t *file);
+bool tiny_sys_file_valid(const tiny_sys_file_t *file);
+tiny_sys_status_t
+tiny_sys_file_open(tiny_sys_file_t *file, const char *path, tiny_sys_file_mode_t mode);
+void tiny_sys_file_close(tiny_sys_file_t *file);
+bool tiny_sys_file_read(tiny_sys_file_t *file, void *data, uint32_t length, uint32_t *actual);
+bool
+tiny_sys_file_write(tiny_sys_file_t *file, const void *data, uint32_t length, uint32_t *actual);
+bool tiny_sys_file_seek(tiny_sys_file_t *file, int64_t offset, tiny_sys_seek_origin_t origin);
+bool tiny_sys_file_size(const tiny_sys_file_t *file, uint64_t *size);
+bool tiny_sys_file_truncate(tiny_sys_file_t *file);
+bool tiny_sys_file_flush(tiny_sys_file_t *file);
+
+uint64_t tiny_sys_now_milliseconds(void);
+uint64_t tiny_sys_monotonic_milliseconds(void);
+bool tiny_sys_random_bytes(void *data, size_t length);
+
+#endif