234 lines
16 KiB
C
234 lines
16 KiB
C
/* JSON introspection: допустимое имя канала возвращается без потери байтов и с полным escaping. */
|
|
#include "../src/utun_instance.h"
|
|
#include "../src/chat/chat_core.h"
|
|
#include "../src/chat/chat_core_priv.h"
|
|
#include "../lib/debug_config.h"
|
|
#include "../lib/mem.h"
|
|
#include <assert.h>
|
|
#include <string.h>
|
|
|
|
enum fault { F_NONE, F_PREPARE, F_BIND, F_STEP, F_FINALIZE, F_TEXT, F_ALLOCATE };
|
|
static enum fault fault;
|
|
static const char* target;
|
|
static int bind_index, text_column, step_number, fail_sync;
|
|
|
|
static int take(enum fault point, const char* sql, int index) {
|
|
if (fault != point || !target || strncmp(sql, target, strlen(target)) ||
|
|
(point == F_BIND && index != bind_index) || (point == F_TEXT && index != text_column)) return 0;
|
|
if (point == F_STEP && --step_number) return 0;
|
|
fault = F_NONE; DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "JSON test: fault stage=%d index=%d target=%s", point, index, target); return 1;
|
|
}
|
|
extern int __real_sqlite3_prepare_v2(sqlite3*, const char*, int, sqlite3_stmt**, const char**);
|
|
int __wrap_sqlite3_prepare_v2(sqlite3* db, const char* sql, int bytes, sqlite3_stmt** st, const char** tail) {
|
|
if (take(F_PREPARE, sql, 0)) { *st = NULL; return SQLITE_NOMEM; }
|
|
return __real_sqlite3_prepare_v2(db, sql, bytes, st, tail);
|
|
}
|
|
extern int __real_sqlite3_bind_text(sqlite3_stmt*, int, const char*, int, void (*)(void*));
|
|
int __wrap_sqlite3_bind_text(sqlite3_stmt* st, int index, const char* value, int bytes, void (*destroy)(void*)) {
|
|
if (take(F_BIND, sqlite3_sql(st), index)) return SQLITE_NOMEM;
|
|
return __real_sqlite3_bind_text(st, index, value, bytes, destroy);
|
|
}
|
|
extern int __real_sqlite3_bind_int64(sqlite3_stmt*, int, sqlite3_int64);
|
|
int __wrap_sqlite3_bind_int64(sqlite3_stmt* st, int index, sqlite3_int64 value) {
|
|
if (take(F_BIND, sqlite3_sql(st), index)) return SQLITE_NOMEM;
|
|
return __real_sqlite3_bind_int64(st, index, value);
|
|
}
|
|
extern int __real_sqlite3_bind_int(sqlite3_stmt*, int, int);
|
|
int __wrap_sqlite3_bind_int(sqlite3_stmt* st, int index, int value) {
|
|
if (take(F_BIND, sqlite3_sql(st), index)) return SQLITE_NOMEM;
|
|
return __real_sqlite3_bind_int(st, index, value);
|
|
}
|
|
extern int __real_sqlite3_step(sqlite3_stmt*);
|
|
int __wrap_sqlite3_step(sqlite3_stmt* st) {
|
|
if (take(F_STEP, sqlite3_sql(st), 0)) return SQLITE_IOERR;
|
|
return __real_sqlite3_step(st);
|
|
}
|
|
extern int __real_sqlite3_finalize(sqlite3_stmt*);
|
|
int __wrap_sqlite3_finalize(sqlite3_stmt* st) {
|
|
int fail = st && take(F_FINALIZE, sqlite3_sql(st), 0);
|
|
int rc = __real_sqlite3_finalize(st); return fail ? SQLITE_IOERR : rc;
|
|
}
|
|
extern const unsigned char* __real_sqlite3_column_text(sqlite3_stmt*, int);
|
|
const unsigned char* __wrap_sqlite3_column_text(sqlite3_stmt* st, int column) {
|
|
if (take(F_TEXT, sqlite3_sql(st), column)) return NULL;
|
|
return __real_sqlite3_column_text(st, column);
|
|
}
|
|
extern void* __real_u_realloc_impl(void*, uint32_t, const char*);
|
|
void* __wrap_u_realloc_impl(void* ptr, uint32_t bytes, const char* location) {
|
|
if (fault == F_ALLOCATE && strstr(location, "strbuf.c")) { fault = F_NONE; return NULL; }
|
|
return __real_u_realloc_impl(ptr, bytes, location);
|
|
}
|
|
extern void* __real_u_calloc_impl(uint32_t, uint32_t, const char*);
|
|
void* __wrap_u_calloc_impl(uint32_t count, uint32_t bytes, const char* location) {
|
|
if (fail_sync && strstr(location, "db_sync.c")) { fail_sync = 0; return NULL; }
|
|
return __real_u_calloc_impl(count, bytes, location);
|
|
}
|
|
|
|
static int listing(struct UTUN_INSTANCE* inst, const char* channel, unsigned kind, char* out, size_t capacity, size_t* size) {
|
|
if (!kind) return chat_core_get_channels_json(inst, out, capacity, size);
|
|
if (kind == 1) return chat_core_get_members_json(inst, channel, out, capacity, size);
|
|
return chat_core_get_messages_json(inst, channel, 100, 0, out, capacity, size);
|
|
}
|
|
|
|
static void valid_json(sqlite3* db, const char* json, size_t size) {
|
|
sqlite3_stmt* st = NULL; assert(strlen(json) == size);
|
|
assert(sqlite3_prepare_v2(db, "SELECT json_valid(?),json_type(?)", -1, &st, NULL) == SQLITE_OK);
|
|
assert(sqlite3_bind_text(st, 1, json, (int)size, SQLITE_STATIC) == SQLITE_OK &&
|
|
sqlite3_bind_text(st, 2, json, (int)size, SQLITE_STATIC) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW);
|
|
assert(sqlite3_column_int(st, 0) && !strcmp((const char*)sqlite3_column_text(st, 1), "array"));
|
|
assert(sqlite3_finalize(st) == SQLITE_OK);
|
|
}
|
|
|
|
static void text_value(sqlite3* db, const char* json, const char* path, const char* expected) {
|
|
sqlite3_stmt* st = NULL;
|
|
assert(sqlite3_prepare_v2(db, "SELECT json_extract(?1,?2)", -1, &st, NULL) == SQLITE_OK);
|
|
assert(sqlite3_bind_text(st, 1, json, -1, SQLITE_STATIC) == SQLITE_OK && sqlite3_bind_text(st, 2, path, -1, SQLITE_STATIC) == SQLITE_OK);
|
|
assert(sqlite3_step(st) == SQLITE_ROW && sqlite3_column_type(st, 0) == SQLITE_TEXT);
|
|
assert(sqlite3_column_bytes(st, 0) == (int)strlen(expected) && !memcmp(sqlite3_column_text(st, 0), expected, strlen(expected)));
|
|
assert(sqlite3_finalize(st) == SQLITE_OK);
|
|
}
|
|
|
|
static void execute(sqlite3* db, const char* sql) {
|
|
int rc = sqlite3_exec(db, sql, NULL, NULL, NULL);
|
|
if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "JSON fixture: rc=%d: %s", rc, sqlite3_errmsg(db));
|
|
assert(rc == SQLITE_OK);
|
|
}
|
|
|
|
static void set_text(sqlite3* db, const char* sql, const char* value, int bytes) {
|
|
sqlite3_stmt* st = NULL; assert(sqlite3_prepare_v2(db, sql, -1, &st, NULL) == SQLITE_OK);
|
|
assert(sqlite3_bind_text(st, 1, value, bytes, SQLITE_STATIC) == SQLITE_OK && sqlite3_step(st) == SQLITE_DONE);
|
|
assert(sqlite3_finalize(st) == SQLITE_OK);
|
|
}
|
|
|
|
static void rejected(struct UTUN_INSTANCE* inst, const char* channel, unsigned kind) {
|
|
char out[65536]; size_t size = 999; int changes = sqlite3_total_changes(inst->topo_sqlite_db); out[0] = 'x';
|
|
assert(listing(inst, channel, kind, out, sizeof(out), &size) < 0 && fault == F_NONE && !size && !out[0]);
|
|
assert(changes == sqlite3_total_changes(inst->topo_sqlite_db));
|
|
}
|
|
|
|
static void retry(struct UTUN_INSTANCE* inst, const char* channel, unsigned kind) {
|
|
char out[65536]; size_t size = 0; int changes = sqlite3_total_changes(inst->topo_sqlite_db);
|
|
assert(!listing(inst, channel, kind, out, sizeof(out), &size)); valid_json(inst->topo_sqlite_db, out, size);
|
|
assert(changes == sqlite3_total_changes(inst->topo_sqlite_db));
|
|
}
|
|
|
|
static void boundaries(struct UTUN_INSTANCE* inst, const char* channel) {
|
|
uint8_t guarded[65540]; char full[65536];
|
|
for (unsigned kind = 0; kind < 3; kind++) {
|
|
size_t bytes = 0; assert(!listing(inst, channel, kind, full, sizeof(full), &bytes));
|
|
size_t capacities[] = {0, 1, 2, 3, bytes - 1, bytes, bytes + 1};
|
|
for (unsigned c = 0; c < sizeof(capacities) / sizeof(capacities[0]); c++) {
|
|
memset(guarded, 0xa5, sizeof(guarded)); size_t capacity = capacities[c], size = 999;
|
|
int rc = listing(inst, channel, kind, (char*)guarded + 2, capacity, &size);
|
|
assert(guarded[0] == 0xa5 && guarded[1] == 0xa5);
|
|
for (size_t i = capacity + 2; i < sizeof(guarded); i++) assert(guarded[i] == 0xa5);
|
|
if (capacity <= bytes) assert(rc < 0 && !size && (!capacity || !guarded[2]));
|
|
else assert(!rc && size == bytes && !memcmp(guarded + 2, full, bytes + 1));
|
|
}
|
|
size_t size = 99; full[0] = 'x'; assert(listing(NULL, channel, kind, full, sizeof(full), &size) < 0 && !size && !full[0]);
|
|
full[0] = 'x'; assert(listing(inst, channel, kind, full, sizeof(full), NULL) < 0 && !full[0]);
|
|
}
|
|
puts("PASS JSON: zero/tiny/exact capacities,canaries,NULL guards,no truncated success");
|
|
}
|
|
|
|
static void faults(struct UTUN_INSTANCE* inst, const char* channel) {
|
|
const char* queries[] = {"SELECT c.channel_id", "SELECT 1 FROM sqlite_master WHERE type='table' AND name=", "SELECT count(*) FROM \"msg_",
|
|
"SELECT node_id FROM \"peers_", "SELECT p.node_id,n.name", "SELECT family,protocol,address", "SELECT json_object('id',m.id"};
|
|
const unsigned kinds[] = {0, 0, 0, 0, 1, 1, 2}; const int bindings[] = {0, 1, 0, 0, 0, 1, 2}; unsigned cases = 0;
|
|
for (unsigned q = 0; q < sizeof(kinds) / sizeof(kinds[0]); q++) {
|
|
target = queries[q];
|
|
for (enum fault point = F_PREPARE; point <= F_FINALIZE; point++) {
|
|
if (point == F_BIND && !bindings[q]) continue;
|
|
for (int index = 1; index <= (point == F_BIND ? bindings[q] : 1); index++) {
|
|
bind_index = index; step_number = 1; fault = point; rejected(inst, channel, kinds[q]); retry(inst, channel, kinds[q]); cases++;
|
|
}
|
|
}
|
|
}
|
|
const unsigned outer[] = {0, 4, 6};
|
|
for (unsigned i = 0; i < 3; i++) {
|
|
target = queries[outer[i]]; step_number = 2; fault = F_STEP; rejected(inst, channel, i); retry(inst, channel, i); cases++;
|
|
int columns[] = {i == 0 ? 0 : i == 1 ? 1 : 0, i == 0 ? 1 : 4, 3};
|
|
unsigned count = i == 0 ? 3 : i == 1 ? 2 : 1;
|
|
for (unsigned column = 0; column < count; column++) {
|
|
text_column = columns[column]; fault = F_TEXT; rejected(inst, channel, i); retry(inst, channel, i); cases++;
|
|
}
|
|
if (i < 2) { fault = F_ALLOCATE; rejected(inst, channel, i); retry(inst, channel, i); cases++; }
|
|
}
|
|
printf("PASS JSON:%u prepare/bind/step/finalize/text/allocation faults and retries,including failure after a completed row\n", cases);
|
|
}
|
|
|
|
static void corruption(struct UTUN_INSTANCE* inst, const char* channel) {
|
|
sqlite3* db = inst->topo_sqlite_db; const char hidden[] = {'a', 0, 'b'}; char sql[256];
|
|
execute(db, "BEGIN"); set_text(db, "UPDATE channels SET name=?", hidden, sizeof(hidden)); rejected(inst, channel, 0); execute(db, "ROLLBACK");
|
|
execute(db, "BEGIN"); set_text(db, "UPDATE nodes SET name=?", hidden, sizeof(hidden));
|
|
rejected(inst, channel, 0); rejected(inst, channel, 1); execute(db, "ROLLBACK");
|
|
execute(db, "BEGIN"); snprintf(sql, sizeof(sql), "UPDATE peers_%s SET adm_tags=?", channel); set_text(db, sql, hidden, sizeof(hidden));
|
|
rejected(inst, channel, 1); execute(db, "ROLLBACK");
|
|
const char* malformed[] = {"UPDATE nodes SET x25519_pubkey=zeroblob(31)", "UPDATE nodes SET ed25519_pubkey=zeroblob(33)",
|
|
"UPDATE nodes SET x25519_pubkey=printf('%032d',1)", "UPDATE node_addresses SET protocol=9", "UPDATE node_addresses SET address='abcd'"};
|
|
for (unsigned i = 0; i < sizeof(malformed) / sizeof(malformed[0]); i++) {
|
|
execute(db, "BEGIN"); execute(db, malformed[i]); rejected(inst, channel, 1); execute(db, "ROLLBACK");
|
|
}
|
|
execute(db, "BEGIN;UPDATE channels SET channel_id='01'"); rejected(inst, channel, 0); execute(db, "ROLLBACK");
|
|
execute(db, "BEGIN;INSERT INTO ui_state(key,value) VALUES('JSON caller transaction','retained')");
|
|
for (unsigned kind = 0; kind < 3; kind++) { retry(inst, channel, kind); assert(!sqlite3_get_autocommit(db)); }
|
|
target = "SELECT json_object('id',m.id"; fault = F_FINALIZE; rejected(inst, channel, 2); assert(!sqlite3_get_autocommit(db));
|
|
execute(db, "ROLLBACK");
|
|
for (unsigned kind = 0; kind < 3; kind++) retry(inst, channel, kind);
|
|
puts("PASS JSON: hidden NUL,invalid key/address/ID rows rejected;caller transaction preserved on success/failure");
|
|
}
|
|
|
|
static void deferred_channel(struct UTUN_INSTANCE* inst) {
|
|
fail_sync = 1; uint64_t created = chat_core_create_channel_auto(inst, "committed, activation pending"); assert(created && !fail_sync);
|
|
char channel[32]; snprintf(channel, sizeof(channel), "%llu", (unsigned long long)created); assert(!si_find(inst, channel));
|
|
char out[65536]; size_t size = 0; assert(!chat_core_get_channels_json(inst, out, sizeof(out), &size)); valid_json(inst->topo_sqlite_db, out, size);
|
|
sqlite3_stmt* st = NULL;
|
|
assert(sqlite3_prepare_v2(inst->topo_sqlite_db, "SELECT json_extract(value,'$.msgs') FROM json_each(?1) "
|
|
"WHERE json_extract(value,'$.id')=?2", -1, &st, NULL) == SQLITE_OK);
|
|
assert(sqlite3_bind_text(st, 1, out, -1, SQLITE_STATIC) == SQLITE_OK && sqlite3_bind_text(st, 2, channel, -1, SQLITE_STATIC) == SQLITE_OK);
|
|
assert(sqlite3_step(st) == SQLITE_ROW && sqlite3_column_type(st, 0) == SQLITE_INTEGER && !sqlite3_column_int(st, 0));
|
|
assert(sqlite3_finalize(st) == SQLITE_OK && !si_find(inst, channel));
|
|
assert(!chat_core_ensure_channel_ready(inst, channel) && si_find(inst, channel));
|
|
assert(!chat_core_get_channels_json(inst, out, sizeof(out), &size)); valid_json(inst->topo_sqlite_db, out, size);
|
|
puts("PASS JSON: committed channel without history remains visible with zero messages;runtime retry succeeds");
|
|
}
|
|
|
|
int main(void) {
|
|
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); size_t baseline = u_get_allocated_count();
|
|
utun_instance_set_tun_init_enabled(0); struct UASYNC* ua = uasync_create(); assert(ua);
|
|
struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua,
|
|
"[ntp]\nenabled=no\n[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n");
|
|
assert(inst && !utun_core_start(inst) && !chat_service_start(inst));
|
|
char name[64] = "Канал "; size_t name_bytes = strlen(name);
|
|
for (int c = 1; c < 32; c++) name[name_bytes++] = (char)c;
|
|
memcpy(name + name_bytes, " \"\\", 4);
|
|
uint64_t id = chat_core_create_channel_auto(inst, name); assert(id); char channel[32];
|
|
snprintf(channel, sizeof(channel), "%llu", (unsigned long long)id);
|
|
char json[4096]; size_t size = 0; assert(!chat_core_get_channels_json(inst, json, sizeof(json), &size));
|
|
sqlite3_stmt* st = NULL;
|
|
assert(sqlite3_prepare_v2(inst->topo_sqlite_db, "SELECT json_valid(?)", -1, &st, NULL) == SQLITE_OK);
|
|
assert(sqlite3_bind_text(st, 1, json, (int)size, SQLITE_STATIC) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW);
|
|
int valid = sqlite3_column_int(st, 0); assert(sqlite3_finalize(st) == SQLITE_OK);
|
|
if (!valid) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "JSON test: channel name with ASCII01 valid=%d bytes=%zu", valid, size);
|
|
assert(valid); text_value(inst->topo_sqlite_db, json, "$[0].name", name);
|
|
char node_name[256]; memset(node_name, '"', sizeof(node_name) - 1); node_name[sizeof(node_name) - 1] = 0;
|
|
set_text(inst->topo_sqlite_db, "UPDATE nodes SET name=?", node_name, -1);
|
|
char tags[8193]; memset(tags, 'a', sizeof(tags) - 1); memcpy(tags, "{\"pad\":\"", 8);
|
|
memcpy(tags + sizeof(tags) - 3, "\"}", 3); char sql[512]; snprintf(sql, sizeof(sql), "UPDATE peers_%s SET adm_tags=?", channel);
|
|
set_text(inst->topo_sqlite_db, sql, tags, -1);
|
|
snprintf(sql, sizeof(sql), "DELETE FROM node_addresses;INSERT INTO node_addresses(node_id,family,protocol,address,port,rtt) VALUES"
|
|
"(%lld,4,1,x'7f000001',4000,7),(%lld,4,6,x'7f000001',443,8),(%lld,6,2,x'00000000000000000000000000000001',4001,9)",
|
|
(long long)inst->node_id, (long long)inst->node_id, (long long)inst->node_id); execute(inst->topo_sqlite_db, sql);
|
|
const char message[] = "Текст \x01 \"\\\n";
|
|
struct chat_msg_submit req = {.inst = inst, .data = (uint8_t*)message, .data_len = sizeof(message) - 1};
|
|
strcpy(req.channel_id, channel); strcpy(req.content_type, "text/plain"); assert(!chat_core_submit_message(inst, &req));
|
|
char full[65536]; assert(!chat_core_get_members_json(inst, channel, full, sizeof(full), &size)); valid_json(inst->topo_sqlite_db, full, size);
|
|
text_value(inst->topo_sqlite_db, full, "$[0].name", node_name); text_value(inst->topo_sqlite_db, full, "$[0].adm_tags", tags);
|
|
text_value(inst->topo_sqlite_db, full, "$[0].addrs[0].ip", "127.0.0.1");
|
|
text_value(inst->topo_sqlite_db, full, "$[0].addrs[1].proto", "TCP+REALITY"); text_value(inst->topo_sqlite_db, full, "$[0].addrs[2].ip", "::1");
|
|
assert(!chat_core_get_channels_json(inst, full, sizeof(full), &size)); text_value(inst->topo_sqlite_db, full, "$[0].owner_name", node_name);
|
|
boundaries(inst, channel); faults(inst, channel); corruption(inst, channel); deferred_channel(inst);
|
|
utun_instance_destroy(inst); uasync_poll(ua, 0); uasync_destroy(ua, 0); assert(u_get_allocated_count() == baseline);
|
|
puts("PASS JSON full escaping,complete outputs,SQL faults,deferred channel;allocations restored"); return 0;
|
|
}
|