Browse Source
Delete local conversations atomically, retain replay protection and sequence counters, and guard asynchronous media work by history generation. Add Android avatar menu and confirmation, persistent file cleanup, and regression coverage. Fix unnamed member wire padding and compact Android member action spacing. Validation: 158 core tests passed; targeted DM tests, desktop build, Android unit tests and debug APK build passed.stream-multiview
38 changed files with 1005 additions and 126 deletions
@ -0,0 +1,177 @@
|
||||
#include "dm_history.h" |
||||
#include "dm_core.h" |
||||
#include "dm_crypto.h" |
||||
#include "../utun_instance.h" |
||||
#include "../../lib/debug_config.h" |
||||
#include <sqlite3.h> |
||||
#include <openssl/sha.h> |
||||
#include <stdlib.h> |
||||
#include <string.h> |
||||
#include <stdio.h> |
||||
|
||||
static int history_error(struct UTUN_INSTANCE* inst, const char* operation, const char* conv) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM history: %s conv=%s sql=%s", operation, conv ? conv : "", |
||||
inst && inst->topo_sqlite_db ? sqlite3_errmsg(inst->topo_sqlite_db) : "database unavailable"); |
||||
return -1; |
||||
} |
||||
|
||||
int dm_history_init(struct UTUN_INSTANCE* inst) { |
||||
const char* sql = |
||||
"CREATE TABLE IF NOT EXISTS dm_history(conv_id TEXT PRIMARY KEY,generation INTEGER NOT NULL,hidden INTEGER NOT NULL);" |
||||
"CREATE TABLE IF NOT EXISTS dm_deleted_messages(conv_id TEXT NOT NULL,dir INTEGER NOT NULL,seq INTEGER NOT NULL," |
||||
"body_hash BLOB NOT NULL,media_id BLOB,media_receipt BLOB,PRIMARY KEY(conv_id,dir,seq));" |
||||
"CREATE TABLE IF NOT EXISTS dm_local_files(path TEXT PRIMARY KEY,conv_id TEXT NOT NULL,generation INTEGER NOT NULL," |
||||
"busy INTEGER NOT NULL DEFAULT 0,retry_at INTEGER NOT NULL DEFAULT 0);" |
||||
"UPDATE dm_local_files SET busy=0;"; |
||||
if (sqlite3_exec(inst->topo_sqlite_db, sql, NULL, NULL, NULL) != SQLITE_OK) return history_error(inst, "init", NULL); |
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "DM history: initialized persistent generations, replay records and file cleanup"); |
||||
return 0; |
||||
} |
||||
|
||||
uint64_t dm_history_generation(struct UTUN_INSTANCE* inst, const char* conv) { |
||||
sqlite3_stmt* st = NULL; |
||||
if (!inst || !inst->topo_sqlite_db || !conv || sqlite3_prepare_v2(inst->topo_sqlite_db, |
||||
"SELECT COALESCE(h.generation,1) FROM dm_conversations c LEFT JOIN dm_history h USING(conv_id) WHERE c.conv_id=?", |
||||
-1, &st, NULL) != SQLITE_OK) { history_error(inst, "generation prepare", conv); return 0; } |
||||
sqlite3_bind_text(st, 1, conv, -1, SQLITE_STATIC); |
||||
int rc = sqlite3_step(st); |
||||
uint64_t generation = rc == SQLITE_ROW ? (uint64_t)sqlite3_column_int64(st, 0) : 0; |
||||
if (rc != SQLITE_ROW && rc != SQLITE_DONE) history_error(inst, "generation read", conv); |
||||
sqlite3_finalize(st); |
||||
return generation; |
||||
} |
||||
|
||||
int dm_history_current(struct UTUN_INSTANCE* inst, const char* conv, uint64_t generation) { |
||||
sqlite3_stmt* st = NULL; |
||||
if (!inst || !inst->topo_sqlite_db || !conv || !generation || sqlite3_prepare_v2(inst->topo_sqlite_db, |
||||
"SELECT 1 FROM dm_conversations c LEFT JOIN dm_history h USING(conv_id)" |
||||
" WHERE c.conv_id=? AND COALESCE(h.generation,1)=? AND COALESCE(h.hidden,0)=0", -1, &st, NULL) != SQLITE_OK) { |
||||
history_error(inst, "current prepare", conv); return 0; |
||||
} |
||||
sqlite3_bind_text(st, 1, conv, -1, SQLITE_STATIC); sqlite3_bind_int64(st, 2, (sqlite3_int64)generation); |
||||
int rc = sqlite3_step(st), current = rc == SQLITE_ROW; |
||||
if (rc != SQLITE_ROW && rc != SQLITE_DONE) history_error(inst, "current read", conv); |
||||
sqlite3_finalize(st); |
||||
if (!current) DEBUG_DEBUG(DEBUG_CATEGORY_DM, "DM history: stale/hidden target conv=%s generation=%llu", conv, |
||||
(unsigned long long)generation); |
||||
return current; |
||||
} |
||||
|
||||
int dm_history_show(struct UTUN_INSTANCE* inst, const char* conv) { |
||||
sqlite3_stmt* st = NULL; |
||||
if (sqlite3_prepare_v2(inst->topo_sqlite_db, "UPDATE dm_history SET hidden=0 WHERE conv_id=?", -1, &st, NULL) != SQLITE_OK) |
||||
return history_error(inst, "show prepare", conv); |
||||
sqlite3_bind_text(st, 1, conv, -1, SQLITE_STATIC); |
||||
int rc = sqlite3_step(st); sqlite3_finalize(st); |
||||
if (rc != SQLITE_DONE) return history_error(inst, "show", conv); |
||||
return 0; |
||||
} |
||||
|
||||
int dm_history_deleted(struct UTUN_INSTANCE* inst, const uint8_t* body, size_t len, int dir, uint8_t* media_receipt) { |
||||
uint64_t conv, seq; char id[64]; |
||||
if (media_receipt) memset(media_receipt, 0, DM_MEDIA_RECEIPT_SIZE); |
||||
size_t actual; |
||||
if (!dm_msg_len(body, len, &actual) || actual != len) return history_error(inst, "invalid replay body", NULL); |
||||
memcpy(&conv, body, 8); memcpy(&seq, body + 8, 8); |
||||
snprintf(id, sizeof(id), "%llu", (unsigned long long)conv); |
||||
sqlite3_stmt* st = NULL; |
||||
if (sqlite3_prepare_v2(inst->topo_sqlite_db, |
||||
"SELECT body_hash,media_receipt FROM dm_deleted_messages WHERE conv_id=? AND dir=? AND seq=?", -1, &st, NULL) != SQLITE_OK) |
||||
return history_error(inst, "replay prepare", id); |
||||
sqlite3_bind_text(st, 1, id, -1, SQLITE_STATIC); sqlite3_bind_int(st, 2, dir); sqlite3_bind_int64(st, 3, (sqlite3_int64)seq); |
||||
int rc = sqlite3_step(st), result = rc == SQLITE_DONE ? 0 : -1; |
||||
if (rc == SQLITE_ROW) { |
||||
uint8_t hash[32]; SHA256(body, len, hash); |
||||
if (sqlite3_column_bytes(st, 0) == sizeof(hash) && !memcmp(hash, sqlite3_column_blob(st, 0), sizeof(hash))) { |
||||
result = 1; |
||||
if (media_receipt && sqlite3_column_bytes(st, 1) == DM_MEDIA_RECEIPT_SIZE) |
||||
memcpy(media_receipt, sqlite3_column_blob(st, 1), DM_MEDIA_RECEIPT_SIZE); |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DM, "DM history: deleted replay conv=%s dir=%d seq=%llu", id, dir, (unsigned long long)seq); |
||||
} else DEBUG_WARN(DEBUG_CATEGORY_DM, "DM history: conflicting deleted message conv=%s dir=%d seq=%llu", |
||||
id, dir, (unsigned long long)seq); |
||||
} else if (rc != SQLITE_DONE) history_error(inst, "replay read", id); |
||||
sqlite3_finalize(st); |
||||
return result; |
||||
} |
||||
|
||||
/* Каноническое тело восстанавливается из сохранённых полей; у file оно уже есть в dm_files. */ |
||||
static int history_remember(struct UTUN_INSTANCE* inst, const char* conv) { |
||||
sqlite3_stmt* rows = NULL; sqlite3_stmt* put = NULL; |
||||
const char* sql = "SELECT m.dir,m.seq,m.ts,m.author,m.ct,m.data,m.sig,f.body,f.receipt" |
||||
" FROM dm_messages m LEFT JOIN dm_files f ON f.conv_id=m.conv_id AND f.dir=m.dir AND f.seq=m.seq WHERE m.conv_id=?"; |
||||
if (sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &rows, NULL) != SQLITE_OK || |
||||
sqlite3_prepare_v2(inst->topo_sqlite_db, |
||||
"INSERT INTO dm_deleted_messages(conv_id,dir,seq,body_hash,media_id,media_receipt) VALUES(?,?,?,?,?,?)", |
||||
-1, &put, NULL) != SQLITE_OK) goto fail; |
||||
sqlite3_bind_text(rows, 1, conv, -1, SQLITE_STATIC); |
||||
int rc; |
||||
while ((rc = sqlite3_step(rows)) == SQLITE_ROW) { |
||||
uint8_t body[DM_MSG_FIXED_HDR + 255 + 2 + DM_DATA_MAX + 16 + DM_MEDIA_DESC_SIZE + DM_MSG_SIG_SIZE], hash[32]; |
||||
size_t length = 0; |
||||
uint64_t number = strtoull(conv, NULL, 10), seq = (uint64_t)sqlite3_column_int64(rows, 1); |
||||
uint64_t ts = (uint64_t)sqlite3_column_int64(rows, 2), author = (uint64_t)sqlite3_column_int64(rows, 3); |
||||
int ct = sqlite3_column_bytes(rows, 4), data = sqlite3_column_bytes(rows, 5), dir = sqlite3_column_int(rows, 0); |
||||
const char* type = (const char*)sqlite3_column_text(rows, 4); |
||||
if (ct == 4 && type && !memcmp(type, "file", 4)) { |
||||
int bytes = sqlite3_column_bytes(rows, 7); |
||||
if (bytes <= 0 || bytes > sizeof(body)) goto fail; |
||||
length = (size_t)bytes; memcpy(body, sqlite3_column_blob(rows, 7), length); |
||||
} else { |
||||
if (ct > 255 || data < 16 || data > DM_DATA_MAX + 16 || sqlite3_column_bytes(rows, 6) != 64) goto fail; |
||||
memcpy(body, &number, 8); memcpy(body + 8, &seq, 8); memcpy(body + 16, &ts, 8); memcpy(body + 24, &author, 8); |
||||
body[32] = (uint8_t)ct; memcpy(body + 33, type, (size_t)ct); |
||||
uint16_t size = (uint16_t)data; memcpy(body + 33 + ct, &size, 2); |
||||
memcpy(body + 35 + ct, sqlite3_column_blob(rows, 5), (size_t)data); |
||||
memcpy(body + 35 + ct + data, sqlite3_column_blob(rows, 6), 64); length = (size_t)(35 + ct + data + 64); |
||||
} |
||||
size_t actual; |
||||
if (!dm_msg_len(body, length, &actual) || actual != length) goto fail; |
||||
SHA256(body, length, hash); |
||||
sqlite3_reset(put); sqlite3_clear_bindings(put); |
||||
sqlite3_bind_text(put, 1, conv, -1, SQLITE_STATIC); sqlite3_bind_int(put, 2, dir); |
||||
sqlite3_bind_int64(put, 3, (sqlite3_int64)seq); sqlite3_bind_blob(put, 4, hash, sizeof(hash), SQLITE_TRANSIENT); |
||||
const uint8_t* media = dm_message_media(body, length); |
||||
if (media) sqlite3_bind_blob(put, 5, media, 16, SQLITE_TRANSIENT); |
||||
if (sqlite3_column_bytes(rows, 8) == DM_MEDIA_RECEIPT_SIZE) |
||||
sqlite3_bind_blob(put, 6, sqlite3_column_blob(rows, 8), DM_MEDIA_RECEIPT_SIZE, SQLITE_TRANSIENT); |
||||
if (sqlite3_step(put) != SQLITE_DONE) goto fail; |
||||
} |
||||
if (rc != SQLITE_DONE) goto fail; |
||||
sqlite3_finalize(rows); sqlite3_finalize(put); |
||||
return 0; |
||||
fail: |
||||
sqlite3_finalize(rows); sqlite3_finalize(put); |
||||
return history_error(inst, "remember deleted bodies", conv); |
||||
} |
||||
|
||||
int dm_history_delete(struct UTUN_INSTANCE* inst, const char* conv, uint64_t generation) { |
||||
if (!inst || !inst->dm || !inst->dm_media || generation >= INT64_MAX || !dm_history_current(inst, conv, generation)) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_DM, "DM history: delete rejected conv=%s generation=%llu", conv ? conv : "", |
||||
(unsigned long long)generation); return -1; |
||||
} |
||||
sqlite3* db = inst->topo_sqlite_db; sqlite3_stmt* st = NULL; |
||||
if (sqlite3_exec(db, "BEGIN IMMEDIATE", NULL, NULL, NULL) != SQLITE_OK) return history_error(inst, "begin delete", conv); |
||||
if (history_remember(inst, conv) || dm_media_forget_prepare(inst, conv, generation)) goto fail; |
||||
const char* statements[] = { |
||||
"DELETE FROM dm_outbox WHERE conv_id=?", "DELETE FROM dm_messages WHERE conv_id=?", |
||||
"INSERT INTO dm_history(conv_id,generation,hidden) VALUES(?,?,1)" |
||||
" ON CONFLICT(conv_id) DO UPDATE SET generation=excluded.generation,hidden=1" |
||||
}; |
||||
for (size_t i = 0; i < sizeof(statements) / sizeof(statements[0]); i++) { |
||||
if (sqlite3_prepare_v2(db, statements[i], -1, &st, NULL) != SQLITE_OK) goto fail; |
||||
sqlite3_bind_text(st, 1, conv, -1, SQLITE_STATIC); |
||||
if (i == 2) sqlite3_bind_int64(st, 2, (sqlite3_int64)(generation + 1)); |
||||
int rc = sqlite3_step(st); sqlite3_finalize(st); st = NULL; |
||||
if (rc != SQLITE_DONE) goto fail; |
||||
} |
||||
if (sqlite3_exec(db, "COMMIT", NULL, NULL, NULL) != SQLITE_OK) goto fail; |
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "DM history: deleted conv=%s generation=%llu -> %llu; counters retained", conv, |
||||
(unsigned long long)generation, (unsigned long long)(generation + 1)); |
||||
dm_media_forget_committed(inst, conv); |
||||
dm_cancel_pending_history(inst, conv); |
||||
return 0; |
||||
fail: |
||||
history_error(inst, "delete rolled back", conv); sqlite3_finalize(st); |
||||
if (sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL) != SQLITE_OK) history_error(inst, "rollback delete", conv); |
||||
return -1; |
||||
} |
||||
@ -0,0 +1,21 @@
|
||||
/* Локальная история DM: поколения, скрытые беседы и точные отпечатки удалённых сообщений.
|
||||
* Все операции выполняются в uasync. Удаление сохраняет счётчики dm_conversations. */ |
||||
#ifndef DM_HISTORY_H |
||||
#define DM_HISTORY_H |
||||
#include <stdint.h> |
||||
#include <stddef.h> |
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
struct UTUN_INSTANCE; |
||||
int dm_history_init(struct UTUN_INSTANCE* inst); |
||||
uint64_t dm_history_generation(struct UTUN_INSTANCE* inst, const char* conv); |
||||
int dm_history_current(struct UTUN_INSTANCE* inst, const char* conv, uint64_t generation); |
||||
int dm_history_show(struct UTUN_INSTANCE* inst, const char* conv); |
||||
/* 1=точный удалённый повтор, 0=не удалено, -1=конфликт/ошибка. receipt может быть NULL. */ |
||||
int dm_history_deleted(struct UTUN_INSTANCE* inst, const uint8_t* body, size_t len, int dir, uint8_t* media_receipt); |
||||
int dm_history_delete(struct UTUN_INSTANCE* inst, const char* conv, uint64_t generation); |
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
#endif |
||||
@ -0,0 +1,213 @@
|
||||
/* Настоящие DM/SQLite/workers: удаление, повторы, nonce, rollback и поколения. */ |
||||
#include <stdio.h> |
||||
#include <stdlib.h> |
||||
#include <string.h> |
||||
#include <openssl/sha.h> |
||||
#include "../src/utun_instance.h" |
||||
#include "../src/dm/dm_core.h" |
||||
#include "../src/dm/dm_crypto.h" |
||||
#include "../src/dm/dm_history.h" |
||||
#include "../src/chat/chat_core.h" |
||||
#include "../src/chat/chat_event.h" |
||||
#include "../src/media_async/attachment_send.h" |
||||
#include "../lib/mem.h" |
||||
#include "../lib/debug_config.h" |
||||
#include "test_utils.h" |
||||
|
||||
#define REQUIRE(value) do { if (!(value)) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM history test failed line=%d", __LINE__); return 1; } } while (0) |
||||
static int messages, delete_results, delete_success, preparation_failures; |
||||
static struct SC_MYKEYS identities[2]; |
||||
static void event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) { |
||||
(void)inst; |
||||
if (type == CHAT_EVT_DM_MSG_RECEIVED) messages++; |
||||
if (type == CHAT_EVT_ATTACHMENT_PREPARATION && len >= 18 && data[len - 1] == 2) preparation_failures++; |
||||
if (type == CHAT_EVT_DM_DELETE_RESULT && len == 25) { delete_results++; delete_success = data[24]; } |
||||
} |
||||
|
||||
static int scalar(struct UTUN_INSTANCE* inst, const char* sql) { |
||||
sqlite3_stmt* st = NULL; |
||||
if (sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &st, NULL)) return -1; |
||||
int n = sqlite3_step(st) == SQLITE_ROW ? sqlite3_column_int(st, 0) : -1; |
||||
sqlite3_finalize(st); return n; |
||||
} |
||||
|
||||
static int body(struct UTUN_INSTANCE* inst, int seq, uint8_t* out, size_t* len) { |
||||
sqlite3_stmt* st = NULL; |
||||
if (sqlite3_prepare_v2(inst->topo_sqlite_db, "SELECT body FROM dm_outbox WHERE seq=?", -1, &st, NULL)) return -1; |
||||
sqlite3_bind_int(st, 1, seq); int rc = sqlite3_step(st); |
||||
int n = rc == SQLITE_ROW ? sqlite3_column_bytes(st, 0) : -1; |
||||
if (n > 0 && (size_t)n <= *len) { memcpy(out, sqlite3_column_blob(st, 0), (size_t)n); *len = (size_t)n; } |
||||
else rc = SQLITE_ERROR; |
||||
sqlite3_finalize(st); return rc == SQLITE_ROW ? 0 : -1; |
||||
} |
||||
|
||||
static int peer(struct UTUN_INSTANCE* a, struct UTUN_INSTANCE* b, char conv[64]) { |
||||
sqlite3_stmt* st = NULL; |
||||
if (sqlite3_prepare_v2(a->topo_sqlite_db, "INSERT OR REPLACE INTO nodes(node_id,x25519_pubkey,ed25519_pubkey) VALUES(?,?,?)", |
||||
-1, &st, NULL)) return -1; |
||||
sqlite3_bind_int64(st, 1, (sqlite3_int64)b->node_id); sqlite3_bind_blob(st, 2, b->my_keys.public_key, 32, SQLITE_STATIC); |
||||
sqlite3_bind_blob(st, 3, b->my_ed25519_pubkey, 32, SQLITE_STATIC); |
||||
int rc = sqlite3_step(st); sqlite3_finalize(st); |
||||
if (rc != SQLITE_DONE || dm_start(a, b->node_id, b->my_keys.public_key, b->my_ed25519_pubkey, "peer", NULL)) return -1; |
||||
snprintf(conv, 64, "%llu", (unsigned long long)dm_derive_conv_id(a->node_id, b->node_id)); return 0; |
||||
} |
||||
|
||||
static struct UTUN_INSTANCE* start(struct UASYNC* ua, const char* directory, int index) { |
||||
char config[1536], pub[65], priv[65]; |
||||
for (unsigned i = 0; i < 32; i++) { |
||||
snprintf(pub + 2 * i, 3, "%02x", identities[index].public_key[i]); |
||||
snprintf(priv + 2 * i, 3, "%02x", identities[index].private_key[i]); |
||||
} |
||||
snprintf(config, sizeof(config), "[global]\nmy_public_key=%s\nmy_private_key=%s\ndb_path=%s/%d\n" |
||||
"[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n", pub, priv, directory, index); |
||||
struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config); |
||||
if (!inst || utun_core_start(inst) || chat_service_start(inst)) return NULL; |
||||
chat_event_set_handler(inst, event); return inst; |
||||
} |
||||
|
||||
int main(void) { |
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); |
||||
if (getenv("UTUN_TEST_DEBUG")) debug_set_category_level(DEBUG_CATEGORY_DM, DEBUG_LEVEL_DEBUG); |
||||
char directory[512] = "/tmp/utun_dm_history_XXXXXX", conv[64], path[768]; |
||||
REQUIRE(!test_mkdtemp(directory)); |
||||
REQUIRE(!sc_generate_keypair(&identities[0]) && !sc_generate_keypair(&identities[1])); |
||||
struct UASYNC* ua = uasync_create(); REQUIRE(ua); |
||||
struct UTUN_INSTANCE* a = start(ua, directory, 0); struct UTUN_INSTANCE* b = start(ua, directory, 1); |
||||
REQUIRE(a && b && !peer(a, b, conv) && !peer(b, a, conv)); |
||||
REQUIRE(dm_history_generation(b, conv) == 1); |
||||
REQUIRE(!dm_send(a, conv, "text", (const uint8_t*)"one", 3)); |
||||
REQUIRE(!dm_send(a, conv, "text", (const uint8_t*)"two", 3)); |
||||
uint8_t first[2048], second[2048], file_body[2048], receipt[DM_RECEIPT_SIZE]; |
||||
size_t first_len = sizeof(first), second_len = sizeof(second), file_len = sizeof(file_body); |
||||
REQUIRE(!body(a, 1, first, &first_len) && !body(a, 2, second, &second_len)); |
||||
REQUIRE(!dm_accept_message(b, second, second_len, receipt)); /* seq=1 ещё не получен. */ |
||||
REQUIRE(!dm_send(b, conv, "text", (const uint8_t*)"reply", 5)); |
||||
int before = messages; |
||||
REQUIRE(!sqlite3_exec(b->topo_sqlite_db, "CREATE TEMP TRIGGER fail_delete BEFORE DELETE ON dm_messages" |
||||
" BEGIN SELECT RAISE(ABORT,'injected history delete failure'); END", NULL, NULL, NULL)); |
||||
REQUIRE(dm_history_delete(b, conv, 1) < 0 && dm_history_generation(b, conv) == 1); |
||||
REQUIRE(scalar(b, "SELECT count(*) FROM dm_deleted_messages") == 0 && scalar(b, "SELECT count(*) FROM dm_messages") == 2); |
||||
REQUIRE(!sqlite3_exec(b->topo_sqlite_db, "DROP TRIGGER fail_delete", NULL, NULL, NULL)); |
||||
struct dm_delete_req* deletion = u_calloc(1, sizeof(*deletion)); REQUIRE(deletion); |
||||
deletion->inst = b; deletion->generation = 1; deletion->request_id = 123; snprintf(deletion->conv_id, 64, "%s", conv); |
||||
dm_delete_trampoline(deletion); |
||||
REQUIRE(delete_results == 1 && delete_success && dm_history_generation(b, conv) == 2); |
||||
REQUIRE(scalar(b, "SELECT count(*) FROM dm_messages") == 0 && scalar(b, "SELECT count(*) FROM dm_outbox") == 0); |
||||
REQUIRE(scalar(b, "SELECT last_out_seq FROM dm_conversations") == 1); |
||||
char json[8192]; size_t length; |
||||
REQUIRE(!dm_list_conversations_json(b, json, sizeof(json), &length) && !strcmp(json, "[]")); |
||||
REQUIRE(!dm_accept_message(b, second, second_len, receipt) && messages == before); |
||||
REQUIRE(!dm_verify_receipt(a, receipt, second, second_len)); |
||||
REQUIRE(scalar(b, "SELECT count(*) FROM dm_messages") == 0 && scalar(b, "SELECT hidden FROM dm_history") == 1); |
||||
uint8_t conflict[2048]; memcpy(conflict, second, second_len); conflict[16] ^= 1; |
||||
REQUIRE(!sc_ed25519_sign(a->my_ed25519_privkey, conflict, second_len - 64, conflict + second_len - 64)); |
||||
REQUIRE(dm_accept_message(b, conflict, second_len, receipt) < 0); |
||||
REQUIRE(!dm_accept_message(b, first, first_len, receipt) && messages == before + 1); |
||||
REQUIRE(scalar(b, "SELECT hidden FROM dm_history") == 0); /* Впервые доставленное seq=1 остаётся новым. */ |
||||
REQUIRE(!dm_send(b, conv, "text", (const uint8_t*)"after", 5)); |
||||
REQUIRE(scalar(b, "SELECT last_out_seq FROM dm_conversations") == 2); |
||||
puts("PASS: atomic delete/rollback, exact replay receipts/conflicts, out-of-order delivery and preserved outgoing seq"); |
||||
|
||||
snprintf(path, sizeof(path), "%s/source.bin", directory); |
||||
FILE* source = fopen(path, "wb"); REQUIRE(source); |
||||
REQUIRE(fwrite("payload", 1, 7, source) == 7 && !fclose(source)); |
||||
struct attachment_info info = { .kind = ATTACHMENT_FILE, .name = "source.bin" }; |
||||
REQUIRE(!dm_send_file(a, conv, path, &info, 0)); |
||||
for (unsigned i = 0; i < 500 && scalar(a, "SELECT count(*) FROM dm_files") == 0; i++) uasync_poll(ua, 20); |
||||
REQUIRE(!body(a, 3, file_body, &file_len) && !dm_accept_message(b, file_body, file_len, receipt)); |
||||
REQUIRE(!dm_history_delete(b, conv, 2)); |
||||
REQUIRE(!dm_accept_message(b, file_body, file_len, receipt)); |
||||
uint8_t saved[DM_MEDIA_RECEIPT_SIZE]; memset(saved, 0x55, sizeof(saved)); |
||||
REQUIRE(dm_history_deleted(b, file_body, file_len, 0, saved) == 1 && saved[0] == 0); |
||||
REQUIRE(scalar(b, "SELECT count(*) FROM dm_files") == 0); |
||||
|
||||
/* Подписанная квитанция загрузки хранится отдельно от квитанции сообщения. */ |
||||
REQUIRE(!dm_send_file(a, conv, path, &info, 0)); |
||||
for (unsigned i = 0; i < 500 && scalar(a, "SELECT count(*) FROM dm_files") < 2; i++) uasync_poll(ua, 20); |
||||
file_len = sizeof(file_body); REQUIRE(!body(a, 4, file_body, &file_len) && !dm_accept_message(b, file_body, file_len, receipt)); |
||||
uint8_t file_receipt[DM_MEDIA_RECEIPT_SIZE]; |
||||
memcpy(file_receipt, "DMFILE01", 8); memcpy(file_receipt + 8, file_body, 16); memcpy(file_receipt + 24, file_body + 24, 8); |
||||
memcpy(file_receipt + 32, &b->node_id, 8); memcpy(file_receipt + 40, dm_message_media(file_body, file_len), 16); |
||||
SHA256(file_body, file_len, file_receipt + 56); |
||||
REQUIRE(!sc_ed25519_sign(b->my_ed25519_privkey, file_receipt, 88, file_receipt + 88)); |
||||
REQUIRE(!dm_media_verify_receipt(a, file_receipt, file_body, file_len)); |
||||
sqlite3_stmt* complete = NULL; |
||||
REQUIRE(!sqlite3_prepare_v2(b->topo_sqlite_db, "UPDATE dm_files SET receipt=?,ready=1", -1, &complete, NULL)); |
||||
sqlite3_bind_blob(complete, 1, file_receipt, sizeof(file_receipt), SQLITE_STATIC); |
||||
REQUIRE(sqlite3_step(complete) == SQLITE_DONE); sqlite3_finalize(complete); |
||||
REQUIRE(!sqlite3_exec(b->topo_sqlite_db, "CREATE TEMP TRIGGER fail_late_delete BEFORE INSERT ON dm_history" |
||||
" BEGIN SELECT RAISE(ABORT,'injected failure after media manifest'); END", NULL, NULL, NULL)); |
||||
int remembered = scalar(b, "SELECT count(*) FROM dm_deleted_messages"); |
||||
REQUIRE(dm_history_delete(b, conv, 3) < 0 && scalar(b, "SELECT count(*) FROM dm_files") == 1); |
||||
REQUIRE(scalar(b, "SELECT count(*) FROM dm_deleted_messages") == remembered && dm_history_generation(b, conv) == 3); |
||||
REQUIRE(!sqlite3_exec(b->topo_sqlite_db, "DROP TRIGGER fail_late_delete", NULL, NULL, NULL)); |
||||
REQUIRE(!dm_history_delete(b, conv, 3)); |
||||
REQUIRE(dm_history_deleted(b, file_body, file_len, 0, saved) == 1 && !memcmp(saved, file_receipt, sizeof(saved))); |
||||
REQUIRE(!dm_history_delete(a, conv, 1)); |
||||
REQUIRE(!dm_media_accept_receipt(a, file_receipt)); /* Поздний ACK удалённого исходящего файла. */ |
||||
file_receipt[56] ^= 1; |
||||
REQUIRE(!sc_ed25519_sign(b->my_ed25519_privkey, file_receipt, 88, file_receipt + 88)); |
||||
REQUIRE(dm_media_accept_receipt(a, file_receipt) < 0); |
||||
puts("PASS: media metadata rollback, retained real file receipts and exact validation of late outgoing receipts"); |
||||
REQUIRE(!peer(b, a, conv)); |
||||
uint64_t generation = dm_history_generation(b, conv); |
||||
REQUIRE(generation == 4 && !dm_send_file(b, conv, path, &info, 0)); |
||||
REQUIRE(!dm_history_delete(b, conv, generation) && !peer(b, a, conv)); |
||||
REQUIRE(dm_history_delete(b, conv, generation) < 0); /* Старый запрос не удаляет новую историю. */ |
||||
struct dm_send_req* stale = u_calloc(1, sizeof(*stale) + 4); REQUIRE(stale); |
||||
stale->inst = b; stale->generation = generation; stale->data_len = 4; stale->data = (uint8_t*)(stale + 1); |
||||
snprintf(stale->conv_id, sizeof(stale->conv_id), "%s", conv); memcpy(stale->data, "late", 4); dm_send_trampoline(stale); |
||||
for (unsigned i = 0; i < 500 && scalar(b, "SELECT count(*) FROM dm_local_files WHERE busy>0") > 0; i++) uasync_poll(ua, 20); |
||||
REQUIRE(scalar(b, "SELECT count(*) FROM dm_files") == 0 && scalar(b, "SELECT count(*) FROM dm_messages") == 0); |
||||
REQUIRE(!access(path, F_OK)); /* Исходный пользовательский файл не принадлежит задаче. */ |
||||
struct attachment_send_req* req = u_calloc(1, sizeof(*req)); REQUIRE(req); |
||||
req->inst = b; req->is_dm = 1; req->dm_generation = dm_history_generation(b, conv); req->info.kind = ATTACHMENT_VOICE; |
||||
req->preset = 1; req->pcm = u_calloc(48000, sizeof(int16_t)); req->pcm_count = 48000; REQUIRE(req->pcm); |
||||
snprintf(req->target, sizeof(req->target), "%s", conv); attachment_send_trampoline(req); |
||||
generation = dm_history_generation(b, conv); |
||||
REQUIRE(!dm_history_delete(b, conv, generation) && !peer(b, a, conv)); |
||||
for (unsigned i = 0; i < 500 && scalar(b, "SELECT count(*) FROM dm_local_files WHERE busy>0") > 0; i++) uasync_poll(ua, 20); |
||||
REQUIRE(scalar(b, "SELECT count(*) FROM dm_messages") == 0 && scalar(b, "SELECT count(*) FROM dm_files") == 0); |
||||
int failures_before = preparation_failures; |
||||
req = u_calloc(1, sizeof(*req)); REQUIRE(req); |
||||
req->inst = b; req->is_dm = 1; req->dm_generation = generation; req->info.kind = ATTACHMENT_VOICE; |
||||
snprintf(req->target, sizeof(req->target), "%s", conv); attachment_send_trampoline(req); |
||||
struct dm_file_req* old_file = u_calloc(1, sizeof(*old_file)); REQUIRE(old_file); |
||||
old_file->inst = b; old_file->generation = generation; old_file->info = info; |
||||
snprintf(old_file->conv, sizeof(old_file->conv), "%s", conv); snprintf(old_file->path, sizeof(old_file->path), "%s", path); |
||||
dm_send_file_trampoline(old_file); |
||||
REQUIRE(preparation_failures == failures_before); |
||||
puts("PASS: deleted media replays do not register files or fake download receipts; old file/voice workers cannot send into reopened PM"); |
||||
|
||||
/* Перезапуск с занятой durable записью: владельца уже нет, уборка должна продолжиться. */ |
||||
snprintf(path, sizeof(path), "%s/restart-owned.bin", directory); |
||||
source = fopen(path, "wb"); REQUIRE(source); REQUIRE(fputc(42, source) != EOF && !fclose(source)); |
||||
uint64_t current = dm_history_generation(b, conv); |
||||
REQUIRE(!dm_media_file_hold(b, conv, current, path)); |
||||
REQUIRE(!dm_history_delete(b, conv, current)); |
||||
utun_instance_destroy(b); |
||||
b = start(ua, directory, 1); REQUIRE(b); |
||||
REQUIRE(dm_history_generation(b, conv) == current + 1 && !dm_history_current(b, conv, current)); |
||||
REQUIRE(!dm_accept_message(b, first, first_len, receipt)); |
||||
REQUIRE(scalar(b, "SELECT count(*) FROM dm_messages") == 0 && scalar(b, "SELECT last_out_seq FROM dm_conversations") == 2); |
||||
dm_media_forget_committed(b, conv); |
||||
for (unsigned i = 0; i < 500 && !access(path, F_OK); i++) uasync_poll(ua, 20); |
||||
REQUIRE(access(path, F_OK) != 0); |
||||
REQUIRE(!dm_list_conversations_json(b, json, sizeof(json), &length) && !strcmp(json, "[]")); |
||||
puts("PASS: restart retains replay protection, counters and hidden state; durable file cleanup resumes after owner loss"); |
||||
REQUIRE(!peer(b, a, conv) && !dm_send(b, conv, "", (const uint8_t*)"x", 1)); |
||||
first_len = sizeof(first); REQUIRE(!body(b, 3, first, &first_len)); |
||||
REQUIRE(!dm_history_delete(b, conv, dm_history_generation(b, conv))); |
||||
REQUIRE(dm_history_deleted(b, first, first_len, 1, NULL) == 1); |
||||
REQUIRE(!peer(a, b, conv) && !dm_send(a, conv, "file_ab", (const uint8_t*)"x", 1)); |
||||
first_len = sizeof(first); REQUIRE(!body(a, 5, first, &first_len)); |
||||
memcpy(first + 33, "file\0ab", 7); |
||||
REQUIRE(!sc_ed25519_sign(a->my_ed25519_privkey, first, first_len - 64, first + first_len - 64)); |
||||
REQUIRE(!dm_accept_message(b, first, first_len, receipt)); |
||||
REQUIRE(!dm_history_delete(b, conv, dm_history_generation(b, conv))); |
||||
REQUIRE(dm_history_deleted(b, first, first_len, 0, NULL) == 1); |
||||
puts("PASS: replay hashes preserve exact protocol bytes for empty and embedded-NUL content types"); |
||||
utun_instance_destroy(b); utun_instance_destroy(a); uasync_destroy(ua, 0); |
||||
puts("test_dm_history: ALL PASS"); |
||||
return 0; |
||||
} |
||||
Loading…
Reference in new issue