#define WIN32_LEAN_AND_MEAN #include #include #include "storage/store.h" #include #include #include #define RECORD_HEADER_SIZE 24U #define RECORD_VERSION 1U #define RECORD_PUT 1U #define RECORD_DEL 2U #define RECORD_MARKER 3U #define RECORD_PAYLOAD_MAX (TINY_CONTENT_MAX + 16384U) #define SNAPSHOT_HEADER_SIZE 52U struct tiny_store { HANDLE lock; HANDLE log; wchar_t *directory; wchar_t *log_path; wchar_t *snapshot_path; tiny_store_entry_t *entries; size_t entry_count; size_t entry_capacity; uint64_t last_sequence; uint64_t last_ulid_ms; unsigned char last_random[10]; unsigned char generation[16]; bool has_generation; bool recovered_tail; bool snapshot_valid; char last_error[160]; }; 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); } static bool write_all(HANDLE 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) { return false; } position += written; length -= written; } return true; } static bool read_all(HANDLE 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)) { return false; } if (count == 0) { break; } total += count; } *actual = total; return true; } 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; } static void make_ulid(tiny_store_t *store, uint64_t timestamp, char output[27]) { static const char alphabet[] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; unsigned char bytes[16]; unsigned index; 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) { uint64_t fallback = GetTickCount64() ^ timestamp; for (index = 0; index < sizeof(store->last_random); ++index) { store->last_random[index] = (unsigned char)(fallback >> ((index % 8) * 8)); } } } else { timestamp = store->last_ulid_ms; for (index = 10; index-- > 0;) { if (++store->last_random[index] != 0) { break; } } } bytes[0] = (unsigned char)(timestamp >> 40); bytes[1] = (unsigned char)(timestamp >> 32); bytes[2] = (unsigned char)(timestamp >> 24); bytes[3] = (unsigned char)(timestamp >> 16); bytes[4] = (unsigned char)(timestamp >> 8); bytes[5] = (unsigned char)timestamp; memcpy(bytes + 6, store->last_random, 10); for (index = 0; index < 26; ++index) { const unsigned bit = index * 5; unsigned value = 0, part; for (part = 0; part < 5; ++part) { const int source_bit = (int)bit + (int)part - 2; value <<= 1; if (source_bit >= 0) { value |= (bytes[source_bit / 8] >> (7 - source_bit % 8)) & 1U; } } output[index] = alphabet[value]; } output[26] = '\0'; } static bool ensure_entries(tiny_store_t *store) { tiny_store_entry_t *resized; size_t capacity; if (store->entry_count < store->entry_capacity) { return true; } capacity = store->entry_capacity == 0 ? 32 : store->entry_capacity * 2; resized = realloc(store->entries, capacity * sizeof(*resized)); if (resized == NULL) { return false; } store->entries = resized; store->entry_capacity = capacity; return true; } static tiny_store_entry_t * find_entry(tiny_store_t *store, const char *message_id) { size_t index; for (index = 0; index < store->entry_count; ++index) { if (strcmp(store->entries[index].message.message_id, message_id) == 0) { return &store->entries[index]; } } return NULL; } static bool serialize_put(const tiny_message_t *message, unsigned char **payload, uint32_t *length) { size_t session_len = strlen(message->session_id), content_len = strlen(message->content); size_t tool_len = message->metadata.tool_call_id ? strlen(message->metadata.tool_call_id) : 0; size_t total = 8 + 26 + 2 + 16 + session_len + content_len + tool_len; size_t index, offset; unsigned char *data; for (index = 0; index < message->metadata.tag_count; ++index) { total += 4 + strlen(message->metadata.tags[index]); } if (total > UINT32_MAX || total > RECORD_PAYLOAD_MAX) { return false; } data = malloc(total); if (data == NULL) { return false; } offset = 0; 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); offset += 4; put_u32(data + offset, (uint32_t)content_len); offset += 4; put_u32(data + offset, (uint32_t)tool_len); offset += 4; put_u32(data + offset, (uint32_t)message->metadata.tag_count); offset += 4; memcpy(data + offset, message->session_id, session_len); offset += session_len; memcpy(data + offset, message->content, content_len); offset += content_len; if (tool_len > 0) { memcpy(data + offset, message->metadata.tool_call_id, tool_len); offset += tool_len; } 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); offset += 4; memcpy(data + offset, message->metadata.tags[index], tag_len); offset += tag_len; } *payload = data; *length = (uint32_t)total; return true; } static char * copy_field(const unsigned char *data, size_t length) { char *value = malloc(length + 1); if (value != NULL) { memcpy(value, data, length); value[length] = '\0'; } return value; } 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; size_t offset = 0, index; tiny_message_init(message); if (length < 8 + 26 + 2 + 16) { return false; } message->timestamp_ms = (int64_t)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); offset += 4; content_len = get_u32(data + offset); offset += 4; tool_len = get_u32(data + offset); offset += 4; tag_count = get_u32(data + offset); offset += 4; if (!tiny_message_id_valid(message->message_id) || (message->role < TINY_ROLE_USER || message->role > TINY_ROLE_SYSTEM) || (message->metadata.memory_tier < TINY_TIER_SHORT || message->metadata.memory_tier > TINY_TIER_DISCARD) || session_len == 0 || session_len > TINY_SESSION_ID_MAX || content_len == 0 || content_len > TINY_CONTENT_MAX || tool_len > TINY_TOOL_CALL_ID_MAX || tag_count > TINY_TAG_COUNT_MAX || offset + session_len + content_len + tool_len > length) { return false; } message->session_id = copy_field(data + offset, session_len); offset += session_len; message->content = copy_field(data + offset, content_len); offset += content_len; if (tool_len > 0) { message->metadata.tool_call_id = copy_field(data + offset, tool_len); offset += tool_len; } if (message->session_id == NULL || message->content == NULL || (tool_len && !message->metadata.tool_call_id)) { goto fail; } if (tag_count > 0) { message->metadata.tags = calloc(tag_count, sizeof(char *)); if (message->metadata.tags == NULL) { goto fail; } for (index = 0; index < tag_count; ++index) { uint32_t tag_len; if (offset + 4 > length) { goto fail; } tag_len = get_u32(data + offset); offset += 4; if (tag_len == 0 || tag_len > TINY_TAG_MAX || offset + tag_len > length) { goto fail; } message->metadata.tags[index] = copy_field(data + offset, tag_len); offset += tag_len; if (message->metadata.tags[index] == NULL) { goto fail; } message->metadata.tag_count++; } } if (offset != length || !tiny_utf8_valid(message->session_id, session_len) || !tiny_utf8_valid(message->content, content_len)) { goto fail; } return true; fail: tiny_message_free(message); return false; } static tiny_store_status_t append_record_to(HANDLE file, uint8_t type, uint64_t sequence, const unsigned char *payload, uint32_t payload_length, 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))) { return TINY_STORE_IO_ERROR; } return TINY_STORE_OK; } static tiny_store_status_t apply_put(tiny_store_t *store, tiny_message_t *message, uint64_t sequence) { tiny_store_entry_t *entry = find_entry(store, message->message_id); if (entry != NULL) { return TINY_STORE_CORRUPT; } if (!ensure_entries(store)) { return TINY_STORE_NO_MEMORY; } entry = &store->entries[store->entry_count++]; memset(entry, 0, sizeof(*entry)); entry->message = *message; tiny_message_init(message); entry->sequence = sequence; return TINY_STORE_OK; } static void clear_entries(tiny_store_t *store) { size_t index; for (index = 0; index < store->entry_count; ++index) { tiny_message_free(&store->entries[index].message); } store->entry_count = 0; store->last_sequence = 0; } 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)) { return TINY_STORE_IO_ERROR; } while (boundary.QuadPart < file_size.QuadPart) { 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)) { return TINY_STORE_IO_ERROR; } if (actual < sizeof(header)) { goto torn_tail; } 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); if (payload_length > RECORD_PAYLOAD_MAX || (header[5] != RECORD_MARKER && sequence <= store->last_sequence)) { return TINY_STORE_CORRUPT; } payload = malloc(payload_length == 0 ? 1 : payload_length); if (payload == NULL) { return TINY_STORE_NO_MEMORY; } if (!read_all(store->log, payload, payload_length, &actual)) { free(payload); return TINY_STORE_IO_ERROR; } if (actual < payload_length) { free(payload); goto torn_tail; } if (crc32_bytes(payload, payload_length) != expected_crc) { free(payload); return TINY_STORE_CORRUPT; } if (header[5] == RECORD_MARKER) { if (boundary.QuadPart != 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; } free(payload); if (status != TINY_STORE_OK) { return status; } if (header[5] != RECORD_MARKER) { store->last_sequence = sequence; } boundary.QuadPart += 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; unsigned char data[SNAPSHOT_HEADER_SIZE] = { 0 }; LARGE_INTEGER 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) { free(temporary); return TINY_STORE_IO_ERROR; } GetFileSizeEx(store->log, &size); 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)); for (index = 0; ok && index < store->entry_count; ++index) { unsigned char prefix[16], *payload = NULL; uint32_t length = 0; if (!serialize_put(&store->entries[index].message, &payload, &length)) { 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); free(payload); } if (ok) { ok = FlushFileBuffers(file) != 0; } CloseHandle(file); if (ok) { ok = MoveFileExW( temporary, store->snapshot_path, MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0; } free(temporary); store->snapshot_valid = ok; return ok ? TINY_STORE_OK : TINY_STORE_IO_ERROR; } static bool 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) || 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))) { return false; } return memcmp(payload, generation, 16) == 0; } 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); 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) { return false; } 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)) { goto invalid; } 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)) { goto invalid; } memcpy(store->generation, header + 8, 16); store->has_generation = true; for (index = 0; index < count; ++index) { unsigned char prefix[16], *payload; uint32_t length, crc; tiny_message_t message; tiny_store_status_t status; if (!read_all(file, prefix, sizeof(prefix), &actual) || actual != sizeof(prefix)) { goto invalid_entries; } length = get_u32(prefix + 8); crc = get_u32(prefix + 12); if (length > RECORD_PAYLOAD_MAX) { goto invalid_entries; } payload = malloc(length ? length : 1); if (!payload) { goto invalid_entries; } if (!read_all(file, payload, length, &actual) || actual != length || crc32_bytes(payload, length) != crc || !deserialize_put(payload, length, &message)) { free(payload); goto invalid_entries; } free(payload); status = apply_put(store, &message, get_u64(prefix)); tiny_message_free(&message); if (status != TINY_STORE_OK) { goto invalid_entries; } store->last_sequence = get_u64(prefix); } if (store->last_sequence != expected_sequence) { goto invalid_entries; } CloseHandle(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; } tiny_store_status_t tiny_store_open(tiny_store_t **output, const wchar_t *directory) { tiny_store_t *store = calloc(1, sizeof(*store)); wchar_t *lock_path = NULL; tiny_store_status_t status; 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"); if (!store->directory || !store->log_path || !store->snapshot_path || !lock_path) { status = TINY_STORE_NO_MEMORY; goto fail; } if (!CreateDirectoryW(directory, NULL) && GetLastError() != ERROR_ALREADY_EXISTS) { status = TINY_STORE_IO_ERROR; goto fail; } store->lock = CreateFileW( lock_path, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); free(lock_path); lock_path = NULL; if (store->lock == INVALID_HANDLE_VALUE) { status = GetLastError() == ERROR_SHARING_VIOLATION ? TINY_STORE_BUSY : TINY_STORE_IO_ERROR; goto fail; } 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) { status = TINY_STORE_IO_ERROR; goto fail; } { uint64_t replay_offset = 0; (void)load_snapshot(store, &replay_offset); status = replay_log(store, replay_offset); } if (status != TINY_STORE_OK) { goto fail; } *output = store; return TINY_STORE_OK; fail: free(lock_path); tiny_store_close(store); return status; } void tiny_store_close(tiny_store_t *store) { size_t index; if (!store) { return; } if (store->log != INVALID_HANDLE_VALUE) { CloseHandle(store->log); } if (store->lock != INVALID_HANDLE_VALUE) { CloseHandle(store->lock); } for (index = 0; index < store->entry_count; ++index) { tiny_message_free(&store->entries[index].message); } free(store->entries); free(store->directory); free(store->log_path); free(store->snapshot_path); free(store); } const char * tiny_store_status_name(tiny_store_status_t status) { switch (status) { case TINY_STORE_OK: return "OK"; case TINY_STORE_NOT_FOUND: return "NOT_FOUND"; case TINY_STORE_BUSY: return "BUSY"; case TINY_STORE_IO_ERROR: return "IO_ERROR"; case TINY_STORE_CORRUPT: return "CORRUPT_STORE"; default: return "INTERNAL_ERROR"; } } tiny_store_status_t tiny_store_write(tiny_store_t *store, const tiny_message_t *input, tiny_message_t *written) { tiny_message_t message, indexed; unsigned char *payload = NULL; uint32_t length = 0; tiny_store_status_t status; tiny_message_init(&message); tiny_message_init(&indexed); if (tiny_message_copy(&message, input) != 0) { return TINY_STORE_NO_MEMORY; } message.timestamp_ms = (int64_t)now_milliseconds(); make_ulid(store, (uint64_t)message.timestamp_ms, message.message_id); if (!serialize_put(&message, &payload, &length) || !ensure_entries(store) || tiny_message_copy(&indexed, &message) != 0) { free(payload); tiny_message_free(&message); tiny_message_free(&indexed); return TINY_STORE_NO_MEMORY; } status = append_record_to(store->log, RECORD_PUT, store->last_sequence + 1, payload, length, true); free(payload); if (status != TINY_STORE_OK) { tiny_message_free(&message); tiny_message_free(&indexed); set_error(store, "failed to append and flush a PUT record"); return status; } store->last_sequence++; memset(&store->entries[store->entry_count], 0, sizeof(store->entries[store->entry_count])); store->entries[store->entry_count].message = indexed; store->entries[store->entry_count].sequence = store->last_sequence; store->entry_count++; *written = message; return TINY_STORE_OK; } 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); unsigned char payload[34]; tiny_store_status_t status; 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()); status = append_record_to( 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"); return status; } store->last_sequence++; entry->deleted = true; return TINY_STORE_OK; } 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; size_t read_index, write_index = 0; uint64_t removed = 0, max_sequence = 0; tiny_store_status_t status = TINY_STORE_OK; 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) { free(temporary); return TINY_STORE_IO_ERROR; } if (BCryptGenRandom( NULL, store->generation, sizeof(store->generation), BCRYPT_USE_SYSTEM_PREFERRED_RNG) != 0) { CloseHandle(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); for (read_index = 0; read_index < store->entry_count; ++read_index) { if (status == TINY_STORE_OK && !store->entries[read_index].deleted) { unsigned char *payload = NULL; uint32_t length = 0; if (!serialize_put(&store->entries[read_index].message, &payload, &length)) { status = TINY_STORE_NO_MEMORY; break; } status = append_record_to( file, RECORD_PUT, store->entries[read_index].sequence, payload, length, false); free(payload); if (status != TINY_STORE_OK) { break; } if (store->entries[read_index].sequence > max_sequence) { max_sequence = store->entries[read_index].sequence; } } else { removed++; } } if (status == TINY_STORE_OK && !FlushFileBuffers(file)) { status = TINY_STORE_IO_ERROR; } CloseHandle(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)) { 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) { status = TINY_STORE_IO_ERROR; } } if (status == TINY_STORE_OK) { for (read_index = 0; read_index < store->entry_count; ++read_index) { if (store->entries[read_index].deleted) { tiny_message_free(&store->entries[read_index].message); } else { if (write_index != read_index) { store->entries[write_index] = store->entries[read_index]; } write_index++; } } store->entry_count = write_index; store->last_sequence = max_sequence; GetFileSizeEx(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; status = write_snapshot(store); } if (status != TINY_STORE_OK) { set_error(store, "compaction did not complete; the previous valid log is retained when replacement " "had not begun"); } free(temporary); return status; } void tiny_store_health(const tiny_store_t *store, tiny_store_health_t *health) { LARGE_INTEGER size; size_t index; memset(health, 0, sizeof(*health)); if (GetFileSizeEx(store->log, &size)) { health->log_size = (uint64_t)size.QuadPart; } { HANDLE f = CreateFileW(store->snapshot_path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (f != INVALID_HANDLE_VALUE) { if (GetFileSizeEx(f, &size)) { health->snapshot_size = (uint64_t)size.QuadPart; } CloseHandle(f); } } for (index = 0; index < store->entry_count; ++index) { if (store->entries[index].deleted) { health->deleted_messages++; } else { health->active_messages++; } } 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); } size_t tiny_store_entry_count(const tiny_store_t *store) { return 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; }