libchat/crates/c-test/c/alice_bob_test.c

207 lines
7.0 KiB
C
Raw Permalink Normal View History

2026-03-26 22:38:38 +01:00
/*
* c-test platform: Alice-Bob message exchange written entirely in C.
*
* Delivery (queue + callback) is implemented here in C.
* Protocol operations are called through a ClientOps vtable supplied by
* the Rust test harness. This keeps the C archive free of direct references
* to Rust symbols, avoiding linker-ordering issues.
* Persistence is in-memory ephemeral (same as the Rust TestPlatform).
*/
#include "client_ffi.h"
#include <assert.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
/* ------------------------------------------------------------------
* In-memory delivery queue
* ------------------------------------------------------------------ */
/* Protocol messages (intro bundles, ratchet envelopes) are well under 2 KB;
* using modest constants keeps both queues inside a few hundred KB of stack. */
#define MAX_ENVELOPES 32
#define MAX_ENVELOPE_SZ 2048
typedef struct {
uint8_t data[MAX_ENVELOPE_SZ];
size_t len;
} Envelope;
typedef struct {
Envelope items[MAX_ENVELOPES];
int head;
int tail;
int count;
} Queue;
static void queue_init(Queue *q)
{
memset(q, 0, sizeof(*q));
}
static void queue_push(Queue *q, const uint8_t *data, size_t len)
{
assert(q->count < MAX_ENVELOPES && "delivery queue overflow");
assert(len <= MAX_ENVELOPE_SZ && "envelope too large");
memcpy(q->items[q->tail].data, data, len);
q->items[q->tail].len = len;
q->tail = (q->tail + 1) % MAX_ENVELOPES;
q->count++;
}
static int queue_pop(Queue *q, const uint8_t **data_out, size_t *len_out)
{
if (q->count == 0) return 0;
*data_out = q->items[q->head].data;
*len_out = q->items[q->head].len;
q->head = (q->head + 1) % MAX_ENVELOPES;
q->count--;
return 1;
}
/* ------------------------------------------------------------------
* Delivery callback: userdata is the sending client's Queue*.
* ------------------------------------------------------------------ */
static int32_t deliver_cb(
const uint8_t *addr_ptr, size_t addr_len,
const uint8_t *data_ptr, size_t data_len,
void *userdata)
{
(void)addr_ptr; (void)addr_len; /* routing is done manually below */
queue_push((Queue *)userdata, data_ptr, data_len);
return 0;
}
/* ------------------------------------------------------------------
* Helper: pop one envelope from `outbox` and push it into `receiver`.
* ------------------------------------------------------------------ */
static void route(
const ClientOps *ops,
Queue *outbox,
ClientHandle_t *receiver,
PushInboundResult_t *out)
{
const uint8_t *data;
size_t len;
int ok = queue_pop(outbox, &data, &len);
assert(ok && "expected an envelope in the outbox");
ops->push_inbound(receiver, data, len, out);
assert(out->error_code == CLIENT_FFI_OK && "push_inbound failed");
}
/* ------------------------------------------------------------------
* Entry point called from the Rust test harness.
* Returns 0 on success, non-zero on failure.
* ------------------------------------------------------------------ */
int run_alice_bob_test(const ClientOps *ops)
{
Queue alice_outbox, bob_outbox;
queue_init(&alice_outbox);
queue_init(&bob_outbox);
/* ---- Create clients ------------------------------------------------- */
/* Each client's delivery callback pushes to its own outbox. */
ClientHandle_t *alice = ops->create(
(const uint8_t *)"alice", 5, deliver_cb, &alice_outbox);
ClientHandle_t *bob = ops->create(
(const uint8_t *)"bob", 3, deliver_cb, &bob_outbox);
assert(alice && "create returned NULL for alice");
assert(bob && "create returned NULL for bob");
/* ---- Bob generates an intro bundle ---------------------------------- */
CreateIntroResult_t bob_intro;
memset(&bob_intro, 0, sizeof(bob_intro));
ops->create_intro_bundle(bob, &bob_intro);
assert(bob_intro.error_code == CLIENT_FFI_OK);
/* ---- Alice initiates a conversation with Bob ------------------------ */
CreateConvoResult_t alice_convo;
memset(&alice_convo, 0, sizeof(alice_convo));
ops->create_conversation(
alice,
bob_intro.intro_bytes_ptr, bob_intro.intro_bytes_len,
(const uint8_t *)"hello bob", 9,
&alice_convo);
assert(alice_convo.error_code == CLIENT_FFI_OK);
ops->destroy_intro_result(&bob_intro);
/* ---- Route alice -> bob --------------------------------------------- */
PushInboundResult_t recv;
memset(&recv, 0, sizeof(recv));
route(ops, &alice_outbox, bob, &recv);
assert(recv.has_content && "expected content from alice");
assert(recv.is_new_convo && "expected new-conversation flag");
assert(recv.content_len == 9);
assert(memcmp(recv.content_ptr, "hello bob", 9) == 0);
/* Save Bob's convo_id for replies */
uint8_t bob_cid[256];
size_t bob_cid_len = recv.convo_id_len;
assert(bob_cid_len < sizeof(bob_cid));
memcpy(bob_cid, recv.convo_id_ptr, bob_cid_len);
ops->destroy_inbound_result(&recv);
/* ---- Bob replies ---------------------------------------------------- */
int32_t rc = ops->send_message(
bob, bob_cid, bob_cid_len,
(const uint8_t *)"hi alice", 8);
assert(rc == CLIENT_FFI_OK);
/* Route bob -> alice */
memset(&recv, 0, sizeof(recv));
route(ops, &bob_outbox, alice, &recv);
assert(recv.has_content && "expected content from bob");
assert(!recv.is_new_convo && "unexpected new-convo flag");
assert(recv.content_len == 8);
assert(memcmp(recv.content_ptr, "hi alice", 8) == 0);
ops->destroy_inbound_result(&recv);
/* ---- Multiple back-and-forth rounds --------------------------------- */
for (int i = 0; i < 3; i++) {
char msg[32];
int mlen = snprintf(msg, sizeof(msg), "msg %d", i);
rc = ops->send_message(
alice,
alice_convo.convo_id_ptr, alice_convo.convo_id_len,
(const uint8_t *)msg, (size_t)mlen);
assert(rc == CLIENT_FFI_OK);
memset(&recv, 0, sizeof(recv));
route(ops, &alice_outbox, bob, &recv);
assert(recv.has_content);
assert((int)recv.content_len == mlen);
assert(memcmp(recv.content_ptr, msg, (size_t)mlen) == 0);
char reply[32];
int rlen = snprintf(reply, sizeof(reply), "reply %d", i);
rc = ops->send_message(
bob, bob_cid, bob_cid_len,
(const uint8_t *)reply, (size_t)rlen);
assert(rc == CLIENT_FFI_OK);
ops->destroy_inbound_result(&recv);
memset(&recv, 0, sizeof(recv));
route(ops, &bob_outbox, alice, &recv);
assert(recv.has_content);
assert((int)recv.content_len == rlen);
assert(memcmp(recv.content_ptr, reply, (size_t)rlen) == 0);
ops->destroy_inbound_result(&recv);
}
/* ---- Cleanup -------------------------------------------------------- */
ops->destroy_convo_result(&alice_convo);
ops->destroy(alice);
ops->destroy(bob);
return 0;
}