summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt11
-rw-r--r--CMakePresets.json23
-rw-r--r--README.md16
-rw-r--r--docs/architecture.md8
-rw-r--r--docs/implementation-status.md18
-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
-rw-r--r--tests/CMakeLists.txt62
-rw-r--r--tests/audit.cmake42
-rw-r--r--tests/boundary_test.c275
-rw-r--r--tests/domain_test.c65
-rw-r--r--tests/fixtures/health.jsonl1
-rw-r--r--tests/fixtures/smoke-input.jsonl6
-rw-r--r--tests/fixtures/write-once.jsonl3
-rw-r--r--tests/function-coverage.md17
-rw-r--r--tests/lifecycle.cmake1
-rw-r--r--tests/smoke.cmake2
-rw-r--r--tests/storage_test.c130
-rw-r--r--tests/support/test.h48
49 files changed, 3508 insertions, 1203 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0e34b7b..ca15a2f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -11,6 +11,16 @@ set(CMAKE_C_STANDARD 17)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS OFF)
+option(TINY_CONVMEM_ENABLE_ASAN "Enable AddressSanitizer for project and tests" OFF)
+if(TINY_CONVMEM_ENABLE_ASAN)
+ if(NOT MSVC)
+ message(FATAL_ERROR "TINY_CONVMEM_ENABLE_ASAN currently supports MSVC only")
+ endif()
+ string(REPLACE "/RTC1" "" CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG}")
+ add_compile_options(/fsanitize=address /Oy-)
+ add_link_options(/INCREMENTAL:NO)
+endif()
+
include(CTest)
include(cmake/mcpc.cmake)
@@ -19,4 +29,3 @@ add_subdirectory(src)
if(BUILD_TESTING)
add_subdirectory(tests)
endif()
-
diff --git a/CMakePresets.json b/CMakePresets.json
index 4ee51a5..d1cd015 100644
--- a/CMakePresets.json
+++ b/CMakePresets.json
@@ -18,6 +18,16 @@
"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
"VCPKG_TARGET_TRIPLET": "x64-windows"
}
+ },
+ {
+ "name": "windows-vs2026-asan",
+ "displayName": "Windows x64 - Visual Studio 2026 ASan",
+ "description": "Debug build with MSVC AddressSanitizer enabled.",
+ "inherits": "windows-vs2026",
+ "binaryDir": "${sourceDir}/build/windows-vs2026-asan",
+ "cacheVariables": {
+ "TINY_CONVMEM_ENABLE_ASAN": "ON"
+ }
}
],
"buildPresets": [
@@ -30,6 +40,11 @@
"name": "windows-vs2026-release",
"configurePreset": "windows-vs2026",
"configuration": "Release"
+ },
+ {
+ "name": "windows-vs2026-asan",
+ "configurePreset": "windows-vs2026-asan",
+ "configuration": "Debug"
}
],
"testPresets": [
@@ -48,6 +63,14 @@
"output": {
"outputOnFailure": true
}
+ },
+ {
+ "name": "windows-vs2026-asan",
+ "configurePreset": "windows-vs2026-asan",
+ "configuration": "Debug",
+ "output": {
+ "outputOnFailure": true
+ }
}
]
}
diff --git a/README.md b/README.md
index 25a319d..2471d49 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,18 @@ cmake --build --preset windows-vs2026-debug
ctest --preset windows-vs2026-debug
```
+AddressSanitizer構成は独立した`build/windows-vs2026-asan`を使用します。
+
+```powershell
+cmake --preset windows-vs2026-asan
+cmake --build --preset windows-vs2026-asan
+ctest --preset windows-vs2026-asan
+```
+
+単体テストにはUTF-8/UTF-16、unaligned binary codec、allocation失敗、short I/Oと
+操作別I/O失敗が含まれます。自前関数とテストの対応は
+[function coverage map](tests/function-coverage.md)を参照してください。
+
## 起動
保存先は必ず明示します。相対パスも使用できます。同じdata directoryを使用する
@@ -57,7 +69,7 @@ stdoutはMCP JSON-RPC専用、stderrは診断専用です。
## 永続化と復旧
`active.log` はversion付きlittle-endianレコード、payload CRC32、単調sequenceを持ちます。
-PUT/DELは `FlushFileBuffers` が成功するまで `OK` を返しません。末尾で中断したレコード
+PUT/DELはsys層のdurability flushが成功するまで`OK`を返しません。末尾で中断したレコード
だけは正常境界まで自動切詰めし、途中のCRC不一致はデータを保存したまま起動を拒否します。
`rebuild_index` は新しいgeneration markerを持つログを作成後、同じgenerationとログoffsetを
@@ -84,7 +96,7 @@ submoduleのソースは変更しません。詳細は[src/mcp/README.md](src/mc
Visual Studio同梱のclang-formatを使う場合は、PowerShellで次のように整形できます。
```powershell
-$formatter = 'C:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\Llvm\bin\clang-format.exe'
+$formatter = 'C:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\Llvm\x64\bin\clang-format.exe'
$files = rg --files src tests -g '*.c' -g '*.h'
& $formatter -i --style=file $files
```
diff --git a/docs/architecture.md b/docs/architecture.md
index edf2dfa..9bd7c62 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -3,7 +3,7 @@
設計項目ごとの実装・テスト状況は
[設計・実装・テスト状況一覧](implementation-status.md)を参照してください。
-`tiny-convmem-mcp` is a single-process Windows C17 MCP server backed by a
+`tiny-convmem-mcp` is a single-process C17 MCP server backed by a
durable append-only conversation log. The executable owns the stdio dispatch
boundary and uses mcpc's bundled mjson parser through a local compatibility
layer.
@@ -19,7 +19,9 @@ MCP transport and tools -> domain contracts <- storage adapters
- `src/domain` owns conversation and message concepts without depending on
mcpc or a database.
- `src/storage` implements the versioned binary log, recovery, snapshot, and
- compaction using Win32 file APIs.
+ compaction through the platform-neutral `src/sys` API.
+- `src/sys` owns UTF-8 path conversion, files, process locks, durable flush,
+ clocks, and random bytes. Its implementation is the only platform split.
## Implemented prototype contract (v1)
@@ -29,7 +31,7 @@ The six public tools are `write_message`, `read_messages`, `search_messages`,
where it conflicts with this section, the implemented contract is authoritative.
- `write_message` creates the ULID and UTC millisecond timestamp. A PUT or DEL
- succeeds only after `FlushFileBuffers` succeeds.
+ succeeds only after the platform durability flush succeeds.
- Deletion is immediately invisible but remains a tombstone until synchronous
compaction. `metadata.deleted` is not part of immutable Message data.
- Metadata is limited to optional `tool_call_id`, `tags`, and `memory_tier`.
diff --git a/docs/implementation-status.md b/docs/implementation-status.md
index 9812ac9..80c8f96 100644
--- a/docs/implementation-status.md
+++ b/docs/implementation-status.md
@@ -22,8 +22,9 @@
| 構成 | ビルド | CTest | 実施日 | 結果 |
|---|---|---|---|---|
-| Windows x64 / MSVC / Debug | ✅ 実施済 | ✅ 実施済 | 2026-06-29 | 5/5成功 |
-| Windows x64 / MSVC / Release | ✅ 実施済 | ✅ 実施済 | 2026-06-29 | 5/5成功 |
+| Windows x64 / MSVC / Debug | ✅ 実施済 | ✅ 実施済 | 2026-06-29 | 12/12成功 |
+| Windows x64 / MSVC / Release | ✅ 実施済 | ✅ 実施済 | 2026-06-29 | 12/12成功 |
+| Windows x64 / MSVC / ASan | ✅ 実施済 | ✅ 実施済 | 2026-06-29 | 12/12成功 |
| Windows以外 | ⬜ 未実施 | ⬜ 未実施 | ― | v1対象外 |
実行コマンド:
@@ -33,6 +34,8 @@ cmake --build --preset windows-vs2026-debug
ctest --preset windows-vs2026-debug
cmake --build --preset windows-vs2026-release
ctest --preset windows-vs2026-release
+cmake --build --preset windows-vs2026-asan
+ctest --preset windows-vs2026-asan
```
## アーキテクチャとプロセス
@@ -44,10 +47,11 @@ ctest --preset windows-vs2026-release
| Domainのmcpc/Win32非依存 | ✅ 実装済 | `message.*`, `search.*` は標準Cのみ | ✅ 作成済 | ✅ Debug/Release | 依存方向の自動検査は未作成 |
| Repository function table | ⬜ 未実装 | MCP層へ `tiny_store_t` を直接渡す | ⬜ 未作成 | ⬜ 未実施 | ストレージ差替え契約は未抽象化 |
| `--data-dir <path>` 必須化 | ✅ 実装済 | 相対パスを絶対化し、不足時は終了コード2 | ✅ 作成済 | ✅ Debug/Release | `--help` の自動テストは未作成 |
-| data directory自動作成 | ✅ 実装済 | `CreateDirectoryW` | 🟡 間接テスト | ✅ Debug/Release | 深い親ディレクトリの再帰作成は未実装 |
+| data directory自動作成 | ✅ 実装済 | UTF-8 `tiny_sys_directory_create` | 🟡 間接テスト | ✅ Debug/Release | 深い親ディレクトリの再帰作成は未実装 |
| data directory排他ロック | ✅ 実装済 | `store.lock` を共有なしで保持 | ✅ 作成済 | ✅ Debug/Release | 複数writerはv1対象外 |
| stdoutをJSON-RPC専用、stderrを診断専用 | ✅ 実装済 | プロトコル応答と起動エラーを分離 | 🟡 間接テスト | ✅ Debug/Release | stderr汚染の専用テストは未作成 |
-| Windows x64/MSVC対応 | ✅ 実装済 | Win32 file API、BCrypt、CMake preset | ✅ 作成済 | ✅ Debug/Release | Windows SDK差異のCIマトリクスは未作成 |
+| OS依存処理の分離 | ✅ 実装済 | `src/sys/system.c`だけでWin32/POSIXを切替え、公開APIはUTF-8・opaque handle | ✅ 作成済 | ✅ Windows Debug/Release | POSIX backendのCI実行は未作成 |
+| Windows x64/MSVC対応 | ✅ 実装済 | sys層のWin32 backendとCMake preset | ✅ 作成済 | ✅ Debug/Release | Windows SDK差異のCIマトリクスは未作成 |
## MCP互換境界と外部契約
@@ -137,7 +141,7 @@ ctest --preset windows-vs2026-release
| generation marker | ✅ 実装済 | 🟡 lifecycleで間接確認 | ✅ Debug/Release | marker単独のbinary testは未作成 |
| 64-bit file offset API | ✅ 実装済 | ⬜ 未作成 | ⬜ 未実施 | 4 GiB超のsparse file試験は未作成 |
| record長上限とoverflow防止 | ✅ 実装済 | ⬜ 未作成 | ⬜ 未実施 | 巨大length/整数overflow fixtureが必要 |
-| PUT/DEL後の `FlushFileBuffers` | ✅ 実装済 | 🟡 API成功経路のみ | ✅ Debug/Release | flush失敗fault injection、実電源断は未実施 |
+| PUT/DEL後のdurability flush | ✅ 実装済 | 🟡 sys API成功経路のみ | ✅ Debug/Release | flush失敗fault injection、実電源断は未実施 |
| 成功前のメモリ確保 | ✅ 実装済 | ⬜ 未作成 | ⬜ 未実施 | allocation failure注入は未作成 |
| OS書込み後・flush前クラッシュ | 🟡 コード上復旧可能 | ⬜ 未作成 | ⬜ 未実施 | プロセス強制終了/電源断試験が必要 |
| record offset/lengthだけを保持するIndexEntry | ⬜ 未実装 | ⬜ 未作成 | ⬜ 未実施 | 現在はMessage全体をメモリ保持 |
@@ -196,6 +200,8 @@ ctest --preset windows-vs2026-release
| `tiny-convmem-mcp.storage` | Unit/Integration | ✅ 作成済 | ✅ 成功 | ✅ 成功 | lock、write/reopen、ULID順、temp失敗、CRC破損拒否 |
| `tiny-convmem-mcp.lifecycle` | MCP/Storage E2E | ✅ 作成済 | ✅ 成功 | ✅ 成功 | delete、不可視化、compaction、本文除去、不正snapshot、末尾復旧 |
| `tiny-convmem-mcp.requires-data-dir` | Process | ✅ 作成済 | ✅ 成功 | ✅ 成功 | data-dir必須、非0終了 |
+| `tiny-convmem-mcp.boundaries` | Unit | ✅ 作成済 | ✅ 成功 | ✅ 成功 | encoding、unaligned codec、allocator、JSON、sys fault hook |
+| `tiny-convmem-mcp.audit` | Static | ✅ 作成済 | ✅ 成功 | ✅ 成功 | clang-format、diff check、goto、OS依存隔離 |
## 未実装・テスト不足の優先バックログ
@@ -224,4 +230,4 @@ ctest --preset windows-vs2026-release
| pagination | ➖ 対象外 | `read_messages` は現状フル履歴契約 |
| 複数writer/network server | ➖ 対象外 | data directoryを単一プロセスが排他所有 |
| SSD/バックアップの安全消去 | ➖ 対象外 | filesystem compactionだけでは保証不能 |
-| Linux/macOS | ➖ 対象外 | Win32 durability/atomic replacementを採用 |
+| Linux/macOS | 🟡 backend実装・未保証 | POSIX file/lock/clock/random実装あり。CI・実機検証は未実施 |
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
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 1407fc7..ad314eb 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -16,19 +16,43 @@ add_executable(tiny-convmem-domain-test
)
target_include_directories(tiny-convmem-domain-test PRIVATE "${PROJECT_SOURCE_DIR}/src")
target_compile_features(tiny-convmem-domain-test PRIVATE c_std_17)
+target_link_libraries(tiny-convmem-domain-test PRIVATE tiny-convmem-base)
if(MSVC)
target_compile_options(tiny-convmem-domain-test PRIVATE /utf-8 /W4)
endif()
add_test(NAME tiny-convmem-mcp.domain COMMAND tiny-convmem-domain-test)
+add_executable(tiny-convmem-boundary-test
+ boundary_test.c
+ "${PROJECT_SOURCE_DIR}/src/mcp/json.c"
+ "${PROJECT_SOURCE_DIR}/src/storage/codec.c"
+)
+target_include_directories(tiny-convmem-boundary-test PRIVATE
+ "${PROJECT_SOURCE_DIR}/src"
+ "${CMAKE_CURRENT_SOURCE_DIR}"
+)
+target_compile_features(tiny-convmem-boundary-test PRIVATE c_std_17)
+target_link_libraries(tiny-convmem-boundary-test PRIVATE tiny-convmem-sys tiny-convmem-base)
+if(MSVC)
+ target_compile_options(tiny-convmem-boundary-test PRIVATE /utf-8 /W4)
+endif()
+add_test(NAME tiny-convmem-mcp.boundaries COMMAND tiny-convmem-boundary-test)
+foreach(boundary_group allocator encoding codec json sys)
+ add_test(
+ NAME "tiny-convmem-mcp.${boundary_group}"
+ COMMAND tiny-convmem-boundary-test "${boundary_group}"
+ )
+endforeach()
+
add_executable(tiny-convmem-storage-test
storage_test.c
"${PROJECT_SOURCE_DIR}/src/domain/message.c"
+ "${PROJECT_SOURCE_DIR}/src/storage/codec.c"
"${PROJECT_SOURCE_DIR}/src/storage/store.c"
)
target_include_directories(tiny-convmem-storage-test PRIVATE "${PROJECT_SOURCE_DIR}/src")
target_compile_features(tiny-convmem-storage-test PRIVATE c_std_17)
-target_link_libraries(tiny-convmem-storage-test PRIVATE bcrypt)
+target_link_libraries(tiny-convmem-storage-test PRIVATE tiny-convmem-sys)
if(MSVC)
target_compile_options(tiny-convmem-storage-test PRIVATE /utf-8 /W4)
endif()
@@ -47,3 +71,39 @@ set_tests_properties(tiny-convmem-mcp.lifecycle PROPERTIES TIMEOUT 15)
add_test(NAME tiny-convmem-mcp.requires-data-dir COMMAND $<TARGET_FILE:tiny-convmem-mcp>)
set_tests_properties(tiny-convmem-mcp.requires-data-dir PROPERTIES WILL_FAIL TRUE)
+
+find_package(Git REQUIRED)
+find_program(
+ CLANG_FORMAT_EXECUTABLE
+ NAMES clang-format
+ HINTS "C:/Program Files/Microsoft Visual Studio/18/Community/VC/Tools/Llvm/x64/bin"
+)
+add_test(
+ NAME tiny-convmem-mcp.audit
+ COMMAND "${CMAKE_COMMAND}"
+ "-DSOURCE_DIR=${PROJECT_SOURCE_DIR}"
+ "-DGIT_EXECUTABLE=${GIT_EXECUTABLE}"
+ "-DCLANG_FORMAT_EXECUTABLE=${CLANG_FORMAT_EXECUTABLE}"
+ -P "${CMAKE_CURRENT_SOURCE_DIR}/audit.cmake"
+)
+
+if(TINY_CONVMEM_ENABLE_ASAN AND MSVC)
+ get_filename_component(TINY_CONVMEM_MSVC_BIN_DIR "${CMAKE_C_COMPILER}" DIRECTORY)
+ set_property(
+ TEST
+ tiny-convmem-mcp.smoke
+ tiny-convmem-mcp.domain
+ tiny-convmem-mcp.boundaries
+ tiny-convmem-mcp.allocator
+ tiny-convmem-mcp.encoding
+ tiny-convmem-mcp.codec
+ tiny-convmem-mcp.json
+ tiny-convmem-mcp.sys
+ tiny-convmem-mcp.storage
+ tiny-convmem-mcp.lifecycle
+ tiny-convmem-mcp.requires-data-dir
+ tiny-convmem-mcp.audit
+ APPEND PROPERTY ENVIRONMENT_MODIFICATION
+ "PATH=path_list_prepend:${TINY_CONVMEM_MSVC_BIN_DIR}"
+ )
+endif()
diff --git a/tests/audit.cmake b/tests/audit.cmake
new file mode 100644
index 0000000..9713f57
--- /dev/null
+++ b/tests/audit.cmake
@@ -0,0 +1,42 @@
+file(GLOB_RECURSE PROJECT_C_SOURCES
+ "${SOURCE_DIR}/src/*.c"
+ "${SOURCE_DIR}/src/*.h"
+)
+
+foreach(source IN LISTS PROJECT_C_SOURCES)
+ file(READ "${source}" contents)
+ if(contents MATCHES "(^|[^A-Za-z0-9_])goto([^A-Za-z0-9_]|$)")
+ message(FATAL_ERROR "goto is forbidden in project source: ${source}")
+ endif()
+ if(contents MATCHES "#define[ \t]+static" OR contents MATCHES "#include[ \t]+\"[^\"]+\\.c\"")
+ message(FATAL_ERROR "test-only source visibility hack found: ${source}")
+ endif()
+ string(FIND "${source}" "/src/sys/" sys_position)
+ if(sys_position EQUAL -1 AND
+ contents MATCHES "windows\\.h|GetFullPathNameW|CreateFileW|ReadFile|WriteFile|MoveFileExW|BCryptGenRandom")
+ message(FATAL_ERROR "OS-specific API reference outside src/sys: ${source}")
+ endif()
+endforeach()
+
+if(NOT CLANG_FORMAT_EXECUTABLE STREQUAL "")
+ execute_process(
+ COMMAND "${CLANG_FORMAT_EXECUTABLE}" --dry-run --Werror ${PROJECT_C_SOURCES}
+ RESULT_VARIABLE format_result
+ OUTPUT_VARIABLE format_output
+ ERROR_VARIABLE format_error
+ )
+ if(NOT format_result EQUAL 0)
+ message(FATAL_ERROR "clang-format check failed:\n${format_output}${format_error}")
+ endif()
+endif()
+
+execute_process(
+ COMMAND "${GIT_EXECUTABLE}" diff --check
+ WORKING_DIRECTORY "${SOURCE_DIR}"
+ RESULT_VARIABLE diff_result
+ OUTPUT_VARIABLE diff_output
+ ERROR_VARIABLE diff_error
+)
+if(NOT diff_result EQUAL 0)
+ message(FATAL_ERROR "git diff --check failed:\n${diff_output}${diff_error}")
+endif()
diff --git a/tests/boundary_test.c b/tests/boundary_test.c
new file mode 100644
index 0000000..452e2ee
--- /dev/null
+++ b/tests/boundary_test.c
@@ -0,0 +1,275 @@
+#include <limits.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "support/test.h"
+
+#include "base/allocator.h"
+#include "mcp/json.h"
+#include "storage/codec.h"
+#include "sys/encoding.h"
+#include "sys/system.h"
+
+static void
+test_allocator_boundaries(tiny_test_state_t *state)
+{
+ void *first;
+ void *resized;
+ size_t baseline = tiny_allocator_outstanding();
+ TINY_TEST_CASE(state, "allocator overflow failure positions and balance");
+ TINY_EXPECT(state, tiny_calloc(SIZE_MAX, 2) == NULL);
+ tiny_allocator_fail_after(0);
+ TINY_EXPECT(state, tiny_alloc(1) == NULL);
+ TINY_EXPECT_EQ_U64(state, 1, tiny_allocator_call_count());
+ tiny_allocator_reset();
+ first = tiny_alloc(1);
+ TINY_EXPECT(state, first != NULL);
+ tiny_allocator_fail_after(0);
+ resized = tiny_realloc(first, 2);
+ TINY_EXPECT(state, resized == NULL);
+ tiny_allocator_reset();
+ tiny_free(first);
+ TINY_EXPECT_EQ_U64(state, baseline, tiny_allocator_outstanding());
+ first = tiny_alloc(1);
+ TINY_EXPECT(state, tiny_realloc(first, 0) == NULL);
+ TINY_EXPECT_EQ_U64(state, baseline, tiny_allocator_outstanding());
+ tiny_free(NULL);
+}
+
+static void
+test_encoding_valid(tiny_test_state_t *state)
+{
+ static const struct vector
+ {
+ const char *utf8;
+ size_t utf8_length;
+ uint16_t utf16[3];
+ size_t utf16_length;
+ } vectors[] = {
+ { "", 0, { 0 }, 0 },
+ { "\x7f", 1, { 0x007f }, 1 },
+ { "\xc2\x80", 2, { 0x0080 }, 1 },
+ { "\xdf\xbf", 2, { 0x07ff }, 1 },
+ { "\xe0\xa0\x80", 3, { 0x0800 }, 1 },
+ { "\xed\x9f\xbf", 3, { 0xd7ff }, 1 },
+ { "\xee\x80\x80", 3, { 0xe000 }, 1 },
+ { "\xef\xbf\xbf", 3, { 0xffff }, 1 },
+ { "\xf0\x90\x80\x80", 4, { 0xd800, 0xdc00 }, 2 },
+ { "\xf4\x8f\xbf\xbf", 4, { 0xdbff, 0xdfff }, 2 },
+ };
+ size_t index;
+ TINY_TEST_CASE(state, "encoding valid scalar boundaries");
+ for (index = 0; index < sizeof(vectors) / sizeof(vectors[0]); ++index)
+ {
+ uint16_t *wide = NULL;
+ char *roundtrip = NULL;
+ size_t wide_length = SIZE_MAX, roundtrip_length = SIZE_MAX;
+ TINY_EXPECT(
+ state,
+ tiny_utf8_to_utf16(vectors[index].utf8, vectors[index].utf8_length, &wide, &wide_length)
+ == TINY_ENCODING_OK);
+ TINY_EXPECT_EQ_U64(state, vectors[index].utf16_length, wide_length);
+ TINY_EXPECT_BYTES(state, vectors[index].utf16, wide, wide_length * sizeof(*wide));
+ TINY_EXPECT(state,
+ tiny_utf16_to_utf8(wide, wide_length, &roundtrip, &roundtrip_length)
+ == TINY_ENCODING_OK);
+ TINY_EXPECT_EQ_U64(state, vectors[index].utf8_length, roundtrip_length);
+ TINY_EXPECT_BYTES(state, vectors[index].utf8, roundtrip, roundtrip_length);
+ free(roundtrip);
+ free(wide);
+ }
+}
+
+static void
+test_encoding_invalid(tiny_test_state_t *state)
+{
+ static const unsigned char invalid[][4] = {
+ { 0x80, 0, 0, 0 }, { 0xc0, 0x80, 0, 0 }, { 0xe0, 0x80, 0x80, 0 },
+ { 0xed, 0xa0, 0x80, 0 }, { 0xf0, 0x80, 0x80, 0x80 }, { 0xf4, 0x90, 0x80, 0x80 },
+ };
+ static const size_t lengths[] = { 1, 2, 3, 3, 4, 4 };
+ size_t index;
+ TINY_TEST_CASE(state, "encoding invalid and allocation failure");
+ for (index = 0; index < sizeof(lengths) / sizeof(lengths[0]); ++index)
+ {
+ uint16_t *output = (uint16_t *)(uintptr_t)1;
+ size_t length = SIZE_MAX;
+ TINY_EXPECT(state,
+ tiny_utf8_to_utf16((const char *)invalid[index], lengths[index], &output, &length)
+ == TINY_ENCODING_INVALID);
+ TINY_EXPECT(state, output == NULL && length == 0);
+ }
+ {
+ uint16_t high[] = { 0xd800 };
+ uint16_t low[] = { 0xdc00 };
+ char *output = NULL;
+ size_t length = 0;
+ TINY_EXPECT(state, tiny_utf16_to_utf8(high, 1, &output, &length) == TINY_ENCODING_INVALID);
+ TINY_EXPECT(state, tiny_utf16_to_utf8(low, 1, &output, &length) == TINY_ENCODING_INVALID);
+ }
+ TINY_EXPECT(state, tiny_utf8_to_utf16(NULL, 1, NULL, NULL) == TINY_ENCODING_INVALID);
+ {
+ uint16_t *output = NULL;
+ size_t length = 0;
+ tiny_allocator_fail_after(0);
+ TINY_EXPECT(state, tiny_utf8_to_utf16("a", 1, &output, &length) == TINY_ENCODING_NO_MEMORY);
+ TINY_EXPECT(state, output == NULL && length == 0);
+ tiny_allocator_reset();
+ }
+}
+
+static void
+test_codec_alignment(tiny_test_state_t *state)
+{
+ unsigned char storage[32];
+ size_t offset;
+ TINY_TEST_CASE(state, "codec every alignment offset");
+ for (offset = 0; offset < 16; ++offset)
+ {
+ unsigned char *position = storage + offset;
+ memset(storage, 0, sizeof(storage));
+ tiny_codec_put_u32(position, UINT32_MAX);
+ TINY_EXPECT_EQ_U64(state, UINT32_MAX, tiny_codec_get_u32(position));
+ tiny_codec_put_u64(position, UINT64_MAX);
+ TINY_EXPECT_EQ_U64(state, UINT64_MAX, tiny_codec_get_u64(position));
+ }
+ TINY_EXPECT_EQ_U64(state, 0, tiny_codec_crc32(NULL, 1));
+ TINY_EXPECT_EQ_U64(state, 0, tiny_codec_crc32(NULL, 0));
+ tiny_codec_put_u32(NULL, 1);
+ tiny_codec_put_u64(NULL, 1);
+ TINY_EXPECT_EQ_U64(state, 0, tiny_codec_get_u32(NULL));
+ TINY_EXPECT_EQ_U64(state, 0, tiny_codec_get_u64(NULL));
+ TINY_EXPECT_EQ_U64(state, 0xcbf43926U, tiny_codec_crc32((const unsigned char *)"123456789", 9));
+}
+
+static void
+test_json_boundaries(tiny_test_state_t *state)
+{
+ tiny_json_buffer_t buffer;
+ static const char controls[] = "\b\f\n\r\t\x01";
+ TINY_TEST_CASE(state, "json growth escaping integer and OOM");
+ tiny_json_init(&buffer);
+ tiny_json_init(NULL);
+ tiny_json_free(NULL);
+ TINY_EXPECT(state, !tiny_json_raw(NULL, "x"));
+ TINY_EXPECT(state, !tiny_json_raw(&buffer, NULL));
+ TINY_EXPECT(state, !tiny_json_raw_n(&buffer, NULL, 1));
+ TINY_EXPECT(state, !tiny_json_string(&buffer, NULL));
+ TINY_EXPECT(state, tiny_json_raw_n(&buffer, "", 0));
+ TINY_EXPECT(state, tiny_json_string(&buffer, controls));
+ TINY_EXPECT(state, strstr(buffer.data, "\\b\\f\\n\\r\\t\\u0001") != NULL);
+ TINY_EXPECT(state, tiny_json_i64(&buffer, LLONG_MIN));
+ TINY_EXPECT(state, tiny_json_u64(&buffer, ULLONG_MAX));
+ tiny_json_free(&buffer);
+ tiny_allocator_fail_after(0);
+ tiny_json_init(&buffer);
+ TINY_EXPECT(state, !tiny_json_raw(&buffer, "allocation"));
+ TINY_EXPECT(state, buffer.failed);
+ tiny_json_free(&buffer);
+ tiny_allocator_reset();
+}
+
+static void
+test_sys_boundaries(tiny_test_state_t *state)
+{
+ char *joined;
+ char *directory;
+ char *path;
+ tiny_sys_file_t file;
+ uint32_t actual = UINT32_MAX;
+ uint64_t size = UINT64_MAX;
+ static const unsigned char bytes[] = { 1, 2, 3, 4 };
+ unsigned char received[sizeof(bytes)] = { 0 };
+ TINY_TEST_CASE(state, "sys public boundary values");
+ tiny_sys_test_reset();
+ TINY_EXPECT(state, tiny_sys_temp_directory_create(NULL) == NULL);
+ TINY_EXPECT(state, tiny_sys_path_join(NULL, "name") == NULL);
+ TINY_EXPECT(state, tiny_sys_path_join("directory", NULL) == NULL);
+ joined = tiny_sys_path_join("", "name");
+ TINY_EXPECT(state, joined != NULL && strcmp(joined, "name") == 0);
+ free(joined);
+ tiny_sys_file_init(NULL);
+ TINY_EXPECT(state, !tiny_sys_file_valid(NULL));
+ tiny_sys_file_init(&file);
+ TINY_EXPECT(state, !tiny_sys_file_valid(&file));
+ TINY_EXPECT(state, tiny_sys_file_open(NULL, "unused", TINY_SYS_FILE_READ) == TINY_SYS_ERROR);
+ TINY_EXPECT(state,
+ tiny_sys_file_open(&file, NULL, TINY_SYS_FILE_READ) == TINY_SYS_ERROR
+ && !tiny_sys_file_valid(&file));
+ TINY_EXPECT(state, !tiny_sys_file_seek(&file, 0, (tiny_sys_seek_origin_t)-1));
+ TINY_EXPECT(state, !tiny_sys_file_size(&file, &size));
+ TINY_EXPECT_EQ_U64(state, 0, size);
+ TINY_EXPECT(state, !tiny_sys_file_read(&file, received, 1, &actual));
+ TINY_EXPECT_EQ_U64(state, 0, actual);
+
+ directory = tiny_sys_temp_directory_create("tcm");
+ TINY_EXPECT(state, directory != NULL);
+ path = directory == NULL ? NULL : tiny_sys_path_join(directory, "fault.bin");
+ TINY_EXPECT(state, path != NULL);
+ if (path != NULL)
+ {
+ TINY_EXPECT(state, tiny_sys_file_open(&file, path, TINY_SYS_FILE_WRITE_NEW) == TINY_SYS_OK);
+ tiny_sys_test_max_transfer(1);
+ TINY_EXPECT(state, tiny_sys_file_write(&file, bytes, sizeof(bytes), &actual));
+ TINY_EXPECT_EQ_U64(state, 1, actual);
+ TINY_EXPECT(state, tiny_sys_test_fail_after(TINY_SYS_TEST_FLUSH, 1));
+ TINY_EXPECT(state, !tiny_sys_file_flush(&file));
+ tiny_sys_test_reset();
+ TINY_EXPECT(state, tiny_sys_file_flush(&file));
+ tiny_sys_file_close(&file);
+
+ TINY_EXPECT(state, tiny_sys_test_fail_after(TINY_SYS_TEST_OPEN, 1));
+ TINY_EXPECT(state,
+ tiny_sys_file_open(&file, path, TINY_SYS_FILE_READ) == TINY_SYS_ERROR
+ && !tiny_sys_file_valid(&file));
+ tiny_sys_test_reset();
+ TINY_EXPECT(state, tiny_sys_file_open(&file, path, TINY_SYS_FILE_READ) == TINY_SYS_OK);
+ TINY_EXPECT(state, tiny_sys_test_fail_after(TINY_SYS_TEST_READ, 1));
+ actual = UINT32_MAX;
+ TINY_EXPECT(state, !tiny_sys_file_read(&file, received, sizeof(received), &actual));
+ TINY_EXPECT_EQ_U64(state, 0, actual);
+ tiny_sys_test_reset();
+ tiny_sys_file_close(&file);
+ TINY_EXPECT(state, tiny_sys_file_remove(path));
+ }
+ free(path);
+ if (directory != NULL)
+ {
+ TINY_EXPECT(state, tiny_sys_directory_remove(directory));
+ }
+ free(directory);
+ tiny_sys_test_reset();
+}
+
+int
+main(int argc, char **argv)
+{
+ tiny_test_state_t state = { "initial", 0, 0 };
+ const char *group = argc > 1 ? argv[1] : "all";
+ tiny_allocator_reset();
+ if (strcmp(group, "all") == 0 || strcmp(group, "allocator") == 0)
+ {
+ test_allocator_boundaries(&state);
+ }
+ if (strcmp(group, "all") == 0 || strcmp(group, "encoding") == 0)
+ {
+ test_encoding_valid(&state);
+ test_encoding_invalid(&state);
+ }
+ if (strcmp(group, "all") == 0 || strcmp(group, "codec") == 0)
+ {
+ test_codec_alignment(&state);
+ }
+ if (strcmp(group, "all") == 0 || strcmp(group, "json") == 0)
+ {
+ test_json_boundaries(&state);
+ }
+ if (strcmp(group, "all") == 0 || strcmp(group, "sys") == 0)
+ {
+ test_sys_boundaries(&state);
+ }
+ TINY_TEST_CASE(&state, "allocator leak balance");
+ TINY_EXPECT_EQ_U64(&state, 0, tiny_allocator_outstanding());
+ return tiny_test_finish(&state);
+}
diff --git a/tests/domain_test.c b/tests/domain_test.c
index d50e4bd..4553657 100644
--- a/tests/domain_test.c
+++ b/tests/domain_test.c
@@ -5,6 +5,8 @@
#include <stdlib.h>
#include <string.h>
+#include "base/allocator.h"
+
static int failures;
#define CHECK(value) \
do \
@@ -17,6 +19,18 @@ static int failures;
} \
while (0)
+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;
+}
+
int
main(void)
{
@@ -27,32 +41,44 @@ main(void)
tiny_message_t original, copy;
tiny_search_index_t *search;
size_t *candidates = NULL, candidate_count = 0;
+ size_t failure_index;
+
+ tiny_allocator_reset();
CHECK(tiny_role_parse("user", &role) && role == TINY_ROLE_USER);
+ CHECK(!tiny_role_parse(NULL, &role));
+ CHECK(!tiny_role_parse("user", NULL));
CHECK(!tiny_role_parse("tool", &role));
CHECK(tiny_tier_parse("discard", &tier) && tier == TINY_TIER_DISCARD);
CHECK(!tiny_tier_parse("archive", &tier));
+ CHECK(!tiny_tier_parse(NULL, &tier));
+ CHECK(!tiny_tier_parse("short", NULL));
+ CHECK(tiny_utf8_valid(NULL, 0));
+ CHECK(!tiny_utf8_valid(NULL, 1));
CHECK(tiny_utf8_valid(valid_utf8, strlen(valid_utf8)));
CHECK(!tiny_utf8_valid(invalid_utf8, sizeof(invalid_utf8) - 1));
CHECK(tiny_message_id_valid("01ARZ3NDEKTSV4RRFFQ69G5FAV"));
CHECK(!tiny_message_id_valid("81ARZ3NDEKTSV4RRFFQ69G5FAV"));
CHECK(!tiny_message_id_valid("01ARZ3NDEKTSV4RRFFQ69G5FAI"));
+ CHECK(!tiny_message_id_valid(NULL));
+ CHECK(!tiny_message_id_valid("01ARZ3NDEKTSV4RRFFQ69G5FA"));
+ CHECK(!tiny_message_id_valid("01ARZ3NDEKTSV4RRFFQ69G5FAV0"));
tiny_message_init(&original);
tiny_message_init(&copy);
- original.session_id = _strdup("session");
- original.content = _strdup("content");
+ original.session_id = duplicate_string("session");
+ original.content = duplicate_string("content");
original.role = TINY_ROLE_ASSISTANT;
- strcpy_s(original.message_id, sizeof(original.message_id), "01ARZ3NDEKTSV4RRFFQ69G5FAV");
+ snprintf(original.message_id, sizeof(original.message_id), "%s", "01ARZ3NDEKTSV4RRFFQ69G5FAV");
CHECK(tiny_message_copy(&copy, &original) == 0);
CHECK(copy.session_id != original.session_id);
CHECK(strcmp(copy.content, "content") == 0);
search = tiny_search_index_create();
CHECK(search != NULL);
free(original.content);
- original.content = _strdup("Curry and coffee");
+ original.content = duplicate_string("Curry and coffee");
original.metadata.tags = calloc(1, sizeof(char *));
- original.metadata.tags[0] = _strdup("設計メモ");
+ original.metadata.tags[0] = duplicate_string("設計メモ");
original.metadata.tag_count = 1;
CHECK(tiny_search_index_add(search, &original, 7));
CHECK(tiny_search_index_candidates(search, "CURRY", &candidates, &candidate_count));
@@ -64,8 +90,37 @@ main(void)
CHECK(candidate_count == 1 && candidates[0] == 7);
CHECK(tiny_search_score("設計", &original) > 0.0);
free(candidates);
+
+ CHECK(!tiny_search_index_add(NULL, &original, 0));
+ CHECK(!tiny_search_index_add(search, NULL, 0));
+ CHECK(!tiny_search_index_candidates(NULL, "x", &candidates, &candidate_count));
+ CHECK(!tiny_search_index_candidates(search, NULL, &candidates, &candidate_count));
+ CHECK(!tiny_search_index_candidates(search, "x", NULL, &candidate_count));
+ CHECK(tiny_search_score(NULL, &original) == 0.0);
+ CHECK(tiny_search_score("x", NULL) == 0.0);
+
+ original.metadata.tool_call_id = duplicate_string("tool");
+ original.metadata.tags = realloc(original.metadata.tags, 2 * sizeof(char *));
+ original.metadata.tags[1] = duplicate_string("second");
+ original.metadata.tag_count = 2;
+ for (failure_index = 0; failure_index < 16; ++failure_index)
+ {
+ const size_t baseline = tiny_allocator_outstanding();
+ tiny_message_t swept;
+ tiny_allocator_fail_after(failure_index);
+ if (tiny_message_copy(&swept, &original) == 0)
+ {
+ tiny_message_free(&swept);
+ CHECK(tiny_allocator_outstanding() == baseline);
+ break;
+ }
+ CHECK(tiny_allocator_outstanding() == baseline);
+ }
+ CHECK(failure_index < 16);
+ tiny_allocator_reset();
tiny_search_index_free(search);
tiny_message_free(&original);
tiny_message_free(&copy);
+ CHECK(tiny_allocator_outstanding() == 0);
return failures == 0 ? 0 : 1;
}
diff --git a/tests/fixtures/health.jsonl b/tests/fixtures/health.jsonl
index 8d1a19f..95642eb 100644
--- a/tests/fixtures/health.jsonl
+++ b/tests/fixtures/health.jsonl
@@ -1,3 +1,4 @@
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"lifecycle","version":"1"}}}
+{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"read_messages","arguments":{"session_id":"lifecycle"}}}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_system_health","arguments":{}}}
diff --git a/tests/fixtures/smoke-input.jsonl b/tests/fixtures/smoke-input.jsonl
index 64bf18f..c746dac 100644
--- a/tests/fixtures/smoke-input.jsonl
+++ b/tests/fixtures/smoke-input.jsonl
@@ -1,9 +1,9 @@
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"tiny-convmem-smoke","version":"0.1.0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
-{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"write_message","arguments":{"session_id":"smoke","role":"user","content":"hello durable memory","metadata":{"tags":["hello","memory"],"memory_tier":"short"}}}}
+{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"write_message","arguments":{"session_id":"smoke","role":"user","content":"hello durable memory","metadata_json":"{\"tags\":[\"hello\",\"memory\"],\"memory_tier\":\"short\"}"}}}
{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"read_messages","arguments":{"session_id":"smoke"}}}
-{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"search_messages","arguments":{"query":"durable","limit":10}}}
+{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"search_messages","arguments":{"query":"durable","session_id":"","limit":10}}}
{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"rebuild_index","arguments":{}}}
{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"get_system_health","arguments":{}}}
-{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"write_message","arguments":{"session_id":"smoke","role":"user","content":"must not be stored","unexpected":true}}}
+{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"write_message","arguments":{"session_id":"smoke","role":"user","content":"must not be stored","metadata_json":"{\"unexpected\":true}"}}}
diff --git a/tests/fixtures/write-once.jsonl b/tests/fixtures/write-once.jsonl
index 0378968..1ad02ae 100644
--- a/tests/fixtures/write-once.jsonl
+++ b/tests/fixtures/write-once.jsonl
@@ -1,2 +1,3 @@
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"lifecycle","version":"1"}}}
-{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"write_message","arguments":{"session_id":"lifecycle","role":"user","content":"secret-to-remove","metadata":{"tags":["秘密","remove"],"memory_tier":"long"}}}}
+{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}
+{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"write_message","arguments":{"session_id":"lifecycle","role":"user","content":"secret-to-remove","metadata_json":"{\"tags\":[\"秘密\",\"remove\"],\"memory_tier\":\"long\"}"}}}
diff --git a/tests/function-coverage.md b/tests/function-coverage.md
new file mode 100644
index 0000000..66c2e4a
--- /dev/null
+++ b/tests/function-coverage.md
@@ -0,0 +1,17 @@
+# Function coverage map
+
+`third_party/` is excluded. Static adapters are covered through their owning public API; pure
+byte/encoding logic is exposed through internal headers and tested directly.
+
+| Source | Functions | Test contract |
+|---|---|---|
+| `base/allocator.c` | `should_fail`, `tiny_alloc`, `tiny_calloc`, `tiny_realloc`, `tiny_free`, `tiny_allocator_reset`, `tiny_allocator_fail_after`, `tiny_allocator_call_count`, `tiny_allocator_outstanding` | `boundaries`: zero/overflow, each allocation primitive, injected failure, balance |
+| `sys/encoding.c` | `utf8_codepoint`, `tiny_utf8_to_utf16`, `tiny_utf16_to_utf8` | `boundaries`: all UTF widths, scalar edges, surrogates, overlong/truncated/invalid input, OOM |
+| `storage/codec.c` | `tiny_codec_put_u32`, `tiny_codec_put_u64`, `tiny_codec_get_u32`, `tiny_codec_get_u64`, `tiny_codec_crc32` | `boundaries`: offsets 0–15, integer limits, empty/NULL and known CRC |
+| `sys/system.c` | test-hook helpers; UTF adapters; `native_file`; argument, path, directory, file, clock and random APIs | `boundaries`, `storage`, `smoke`: NULL/invalid handle, Unicode paths, short I/O, operation failure, cleanup |
+| `domain/message.c` | `duplicate_string`, `copy_failed`, all `tiny_message_*`, role/tier parse/name, `tiny_utf8_valid`, `tiny_message_id_valid` | `domain`: enum failure, UTF-8 invalidity, ULID alphabet/length, deep copy and ownership |
+| `domain/search.c` | `decode_codepoints`, `gram_hash`, `find_bucket`, `add_gram`, `index_text`, all `tiny_search_*` | `domain`: ASCII case folding, Unicode grams/tags, candidates, scoring and ownership |
+| `storage/store.c` | error/I/O helpers, ULID, entry growth/search, record codec, replay/snapshot/recovery, all `tiny_store_*` | `storage`, `lifecycle`: lock, append/reopen, corruption, rebuild rollback, Unicode path |
+| `mcp/json.c` | `reserve`, all `tiny_json_*` | `boundaries`: empty append, growth, control escaping, signed/unsigned integer limits, OOM |
+| `mcp/server.c` | line/JSON/parser/result/search helpers, `handle_call`, `tiny_convmem_server_run` | `smoke`, `lifecycle`: initialize, list/call, malformed/EOF behavior, persistence |
+| `app/main.c` | `usage`, `run`, `main` | `requires-data-dir`, `smoke`: missing CLI argument, normal startup and EOF |
diff --git a/tests/lifecycle.cmake b/tests/lifecycle.cmake
index 8d2f97d..e0b1149 100644
--- a/tests/lifecycle.cmake
+++ b/tests/lifecycle.cmake
@@ -24,6 +24,7 @@ endif()
set(delete_input "${DATA_DIR}/delete.jsonl")
file(WRITE "${delete_input}"
"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"lifecycle\",\"version\":\"1\"}}}\n"
+ "{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\",\"params\":{}}\n"
"{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"delete_message\",\"arguments\":{\"message_id\":\"${message_id}\",\"session_id\":\"lifecycle\"}}}\n"
"{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"read_messages\",\"arguments\":{\"session_id\":\"lifecycle\"}}}\n"
"{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"tools/call\",\"params\":{\"name\":\"rebuild_index\",\"arguments\":{}}}\n"
diff --git a/tests/smoke.cmake b/tests/smoke.cmake
index 2481297..3992b6b 100644
--- a/tests/smoke.cmake
+++ b/tests/smoke.cmake
@@ -35,7 +35,7 @@ foreach(expected
"\"name\":\"delete_message\""
"\"name\":\"rebuild_index\""
"\"name\":\"get_system_health\""
- "\"required\":[\"session_id\",\"role\",\"content\"]"
+ "\"metadata_json\""
"\"type\":\"text\""
"\\\"status\\\":\\\"OK\\\""
"\\\"code\\\":\\\"VALIDATION_ERROR\\\""
diff --git a/tests/storage_test.c b/tests/storage_test.c
index bac0f0b..3217095 100644
--- a/tests/storage_test.c
+++ b/tests/storage_test.c
@@ -1,7 +1,5 @@
-#define WIN32_LEAN_AND_MEAN
-#include <windows.h>
-
#include "storage/store.h"
+#include "sys/system.h"
#include <stdio.h>
#include <stdlib.h>
@@ -19,24 +17,69 @@ static int failures;
} \
while (0)
+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;
+}
+
int
-wmain(void)
+main(void)
{
- wchar_t temporary[MAX_PATH], directory[MAX_PATH], log_path[MAX_PATH];
+ char *temporary_root = tiny_sys_temp_directory_create("tcm");
+ char *directory;
+ char *log_path;
+ char *temporary_path;
+ char *lock_path;
+ char *snapshot_path;
tiny_store_t *store = NULL, *second = NULL;
tiny_message_t input, first, second_message;
tiny_store_status_t status;
- HANDLE log;
- HANDLE blocker;
+ tiny_sys_file_t log;
tiny_rebuild_report_t report;
- LARGE_INTEGER position;
unsigned char byte;
- DWORD count;
+ uint32_t count;
+ tiny_store_health_t health;
+ size_t operation;
+ static const tiny_sys_test_operation_t rebuild_failures[] = { TINY_SYS_TEST_OPEN,
+ TINY_SYS_TEST_RANDOM,
+ TINY_SYS_TEST_WRITE,
+ TINY_SYS_TEST_FLUSH,
+ TINY_SYS_TEST_REPLACE };
+
+ CHECK(tiny_store_open(NULL, "unused") == TINY_STORE_IO_ERROR);
+ store = (tiny_store_t *)(uintptr_t)1;
+ CHECK(tiny_store_open(&store, NULL) == TINY_STORE_IO_ERROR && store == NULL);
+ CHECK(tiny_store_entry_count(NULL) == 0);
+ CHECK(tiny_store_entry(NULL, 0) == NULL);
+ memset(&health, 0xff, sizeof(health));
+ tiny_store_health(NULL, &health);
+ CHECK(health.active_messages == 0 && health.log_size == 0);
+
+ CHECK(temporary_root != NULL);
+ if (temporary_root == NULL)
+ {
+ return 1;
+ }
+ directory = tiny_sys_path_join(temporary_root, "日本語-memory");
+ CHECK(directory != NULL && tiny_sys_directory_create(directory) == TINY_SYS_OK);
+ if (directory == NULL)
+ {
+ free(temporary_root);
+ return 1;
+ }
+ log_path = tiny_sys_path_join(directory, "active.log");
+ temporary_path = tiny_sys_path_join(directory, "active.tmp");
+ lock_path = tiny_sys_path_join(directory, "store.lock");
+ snapshot_path = tiny_sys_path_join(directory, "snapshot.bin");
+ CHECK(log_path != NULL && temporary_path != NULL && lock_path != NULL && snapshot_path != NULL);
- CHECK(GetTempPathW(MAX_PATH, temporary) > 0);
- CHECK(GetTempFileNameW(temporary, L"tcm", 0, directory) > 0);
- DeleteFileW(directory);
- CHECK(CreateDirectoryW(directory, NULL) != 0);
status = tiny_store_open(&store, directory);
CHECK(status == TINY_STORE_OK);
CHECK(tiny_store_open(&second, directory) == TINY_STORE_BUSY);
@@ -44,12 +87,12 @@ wmain(void)
tiny_message_init(&input);
tiny_message_init(&first);
tiny_message_init(&second_message);
- input.session_id = _strdup("unit");
- input.content = _strdup("first");
+ input.session_id = duplicate_string("unit");
+ input.content = duplicate_string("first");
input.role = TINY_ROLE_USER;
CHECK(tiny_store_write(store, &input, &first) == TINY_STORE_OK);
free(input.content);
- input.content = _strdup("second");
+ input.content = duplicate_string("second");
CHECK(tiny_store_write(store, &input, &second_message) == TINY_STORE_OK);
CHECK(strcmp(first.message_id, second_message.message_id) < 0);
tiny_message_free(&input);
@@ -60,38 +103,43 @@ wmain(void)
CHECK(tiny_store_open(&store, directory) == TINY_STORE_OK);
CHECK(tiny_store_entry_count(store) == 2);
- swprintf_s(log_path, MAX_PATH, L"%s\\active.tmp", directory);
- blocker = CreateFileW(
- log_path, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
- CHECK(blocker != INVALID_HANDLE_VALUE);
+ for (operation = 0; operation < sizeof(rebuild_failures) / sizeof(rebuild_failures[0]);
+ ++operation)
+ {
+ tiny_sys_test_reset();
+ CHECK(tiny_sys_test_fail_after(rebuild_failures[operation], 1));
+ CHECK(tiny_store_rebuild(store, &report) != TINY_STORE_OK);
+ CHECK(tiny_store_entry_count(store) == 2);
+ tiny_sys_test_reset();
+ (void)tiny_sys_file_remove(temporary_path);
+ }
+ CHECK(tiny_sys_directory_create(temporary_path) == TINY_SYS_OK);
CHECK(tiny_store_rebuild(store, &report) == TINY_STORE_IO_ERROR);
CHECK(tiny_store_entry_count(store) == 2);
- CloseHandle(blocker);
- DeleteFileW(log_path);
+ CHECK(tiny_sys_directory_remove(temporary_path));
tiny_store_close(store);
store = NULL;
- swprintf_s(log_path, MAX_PATH, L"%s\\active.log", directory);
- log = CreateFileW(
- log_path, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
- CHECK(log != INVALID_HANDLE_VALUE);
- position.QuadPart = -1;
- CHECK(SetFilePointerEx(log, position, NULL, FILE_END) != 0);
- CHECK(ReadFile(log, &byte, 1, &count, NULL) != 0 && count == 1);
+ CHECK(tiny_sys_file_open(&log, log_path, TINY_SYS_FILE_READ_WRITE) == TINY_SYS_OK);
+ CHECK(tiny_sys_file_seek(&log, -1, TINY_SYS_SEEK_END));
+ CHECK(tiny_sys_file_read(&log, &byte, 1, &count) && count == 1);
byte ^= 0x5a;
- position.QuadPart = -1;
- CHECK(SetFilePointerEx(log, position, NULL, FILE_CURRENT) != 0);
- CHECK(WriteFile(log, &byte, 1, &count, NULL) != 0 && count == 1);
- CloseHandle(log);
+ CHECK(tiny_sys_file_seek(&log, -1, TINY_SYS_SEEK_CURRENT));
+ CHECK(tiny_sys_file_write(&log, &byte, 1, &count) && count == 1);
+ tiny_sys_file_close(&log);
CHECK(tiny_store_open(&store, directory) == TINY_STORE_CORRUPT);
- DeleteFileW(log_path);
- swprintf_s(log_path, MAX_PATH, L"%s\\active.tmp", directory);
- DeleteFileW(log_path);
- swprintf_s(log_path, MAX_PATH, L"%s\\store.lock", directory);
- DeleteFileW(log_path);
- swprintf_s(log_path, MAX_PATH, L"%s\\snapshot.bin", directory);
- DeleteFileW(log_path);
- RemoveDirectoryW(directory);
+ (void)tiny_sys_file_remove(log_path);
+ (void)tiny_sys_file_remove(temporary_path);
+ (void)tiny_sys_file_remove(lock_path);
+ (void)tiny_sys_file_remove(snapshot_path);
+ (void)tiny_sys_directory_remove(directory);
+ (void)tiny_sys_directory_remove(temporary_root);
+ free(snapshot_path);
+ free(lock_path);
+ free(temporary_path);
+ free(log_path);
+ free(directory);
+ free(temporary_root);
return failures == 0 ? 0 : 1;
}
diff --git a/tests/support/test.h b/tests/support/test.h
new file mode 100644
index 0000000..5de3864
--- /dev/null
+++ b/tests/support/test.h
@@ -0,0 +1,48 @@
+#ifndef TINY_CONVMEM_TEST_SUPPORT_H
+#define TINY_CONVMEM_TEST_SUPPORT_H
+
+#include <stdio.h>
+#include <string.h>
+
+typedef struct tiny_test_state
+{
+ const char *case_name;
+ unsigned checks;
+ unsigned failures;
+} tiny_test_state_t;
+
+#define TINY_TEST_CASE(state, name) ((state)->case_name = (name))
+#define TINY_EXPECT(state, expression) \
+ do \
+ { \
+ (state)->checks++; \
+ if (!(expression)) \
+ { \
+ fprintf(stderr, \
+ "%s:%d [%s] expected %s\n", \
+ __FILE__, \
+ __LINE__, \
+ (state)->case_name, \
+ #expression); \
+ (state)->failures++; \
+ } \
+ } \
+ while (0)
+#define TINY_EXPECT_EQ_U64(state, expected, actual) \
+ TINY_EXPECT((state), (unsigned long long)(expected) == (unsigned long long)(actual))
+#define TINY_EXPECT_BYTES(state, expected, actual, length) \
+ TINY_EXPECT((state), memcmp((expected), (actual), (length)) == 0)
+
+static inline int
+tiny_test_finish(const tiny_test_state_t *state)
+{
+ if (state->failures == 0)
+ {
+ printf("%u checks passed\n", state->checks);
+ return 0;
+ }
+ fprintf(stderr, "%u of %u checks failed\n", state->failures, state->checks);
+ return 1;
+}
+
+#endif