diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5cbd2ad..b873144 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,23 +36,3 @@ jobs: - run: rustup update stable && rustup default stable - run: rustup component add rustfmt - run: cargo fmt --all -- --check - - nim-bindings-test: - name: Nim Bindings Test - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest] - steps: - - uses: actions/checkout@v4 - - run: rustup update stable && rustup default stable - - name: Install Nim via choosenim - run: | - curl https://nim-lang.org/choosenim/init.sh -sSf | sh -s -- -y - echo "$HOME/.nimble/bin" >> $GITHUB_PATH - - run: nimble install -dy - working-directory: nim-bindings - - run: nimble test - working-directory: nim-bindings - - run: nimble pingpong - working-directory: nim-bindings diff --git a/core/conversations/src/api.rs b/core/conversations/src/api.rs deleted file mode 100644 index 47f0a99..0000000 --- a/core/conversations/src/api.rs +++ /dev/null @@ -1,393 +0,0 @@ -// This is the FFI Interface to enable libchat to be used from other languages such as Nim and C. -// This interface makes heavy use of safer_ffi in order to safely move bytes across the FFI. -// -// The following table explains the safer_ffi types in use, and under what circumstances. -// -// - c_slice::Ref<'_, u8> : Borrowed, read-only byte slice for input parameters -// - c_slice::Mut<'_, u8> : Borrowed, mutable byte slice for in/out parameters -// - repr_c::Vec : Owned vector, used for return values (transfers ownership to caller) -// - repr_c::String : Owned string, used for return values (transfers ownership to caller) - -use safer_ffi::{ - String, derive_ReprC, ffi_export, - prelude::{c_slice, repr_c}, -}; - -use sqlite::{ChatStorage, StorageConfig}; - -use crate::{ - context::{Context, Introduction}, - errors::ChatError, - types::ContentData, -}; - -// Must only contain negative values or 0, values cannot be changed once set. -#[repr(i32)] -pub enum ErrorCode { - None = 0, - BadPtr = -1, - BadConvoId = -2, - BadIntro = -3, - NotImplemented = -4, - BufferExceeded = -5, - UnknownError = -6, -} - -pub fn is_ok(error: i32) -> bool { - error == ErrorCode::None as i32 -} - -// ------------------------------------------ -// Exported Functions -// ------------------------------------------ - -/// Opaque wrapper for Context -#[derive_ReprC] -#[repr(opaque)] -pub struct ContextHandle(pub(crate) Context); - -/// Creates a new libchat Ctx -/// -/// # Returns -/// Opaque handle to the store. Must be freed with destroy_context() -#[ffi_export] -pub fn create_context(name: repr_c::String) -> repr_c::Box { - // Deference name to to `str` and then borrow to &str - let store = - ChatStorage::new(StorageConfig::InMemory).expect("in-memory storage should not fail"); - Box::new(ContextHandle(Context::new_with_name(&*name, store))).into() -} - -/// Returns the friendly name of the contexts installation. -/// -#[ffi_export] -pub fn installation_name(ctx: &ContextHandle) -> repr_c::String { - ctx.0.installation_name().to_string().into() -} - -/// Destroys a conversation store and frees its memory -/// -/// # Safety -/// - handle must be a valid pointer from conversation_store_create() -/// - handle must not be used after this call -/// - handle must not be freed twice -#[ffi_export] -pub fn destroy_context(ctx: repr_c::Box) { - drop(ctx); -} - -/// Destroys a repr_c::String and frees its memory -/// -/// # Safety -/// - s must be an owned repr_c::String value returned from a libchat FFI function -/// - s must not be used after this call -/// - s must not be freed twice -#[ffi_export] -pub fn destroy_string(s: repr_c::String) { - drop(s); -} - -/// Creates an intro bundle for sharing with other users -/// -/// # Returns -/// Check error_code field: 0 means success, negative values indicate errors (see ErrorCode). -/// -/// # ABI note -/// The result is written through `out` (Nim's calling convention for large struct returns). -#[ffi_export] -pub fn create_intro_bundle(ctx: &mut ContextHandle, out: &mut CreateIntroResult) { - *out = match ctx.0.create_intro_bundle() { - Ok(v) => CreateIntroResult { - error_code: ErrorCode::None as i32, - intro_bytes: v.into(), - }, - Err(_e) => CreateIntroResult { - error_code: ErrorCode::UnknownError as i32, - intro_bytes: repr_c::Vec::EMPTY, - }, - }; -} - -/// Creates a new private conversation -/// -/// # Returns -/// Returns a struct with payloads that must be sent, the conversation_id that was created. -/// The NewConvoResult must be freed. -/// -/// # ABI note -/// The result is written through `out` (Nim's calling convention for large struct returns). -#[ffi_export] -pub fn create_new_private_convo( - ctx: &mut ContextHandle, - bundle: c_slice::Ref<'_, u8>, - content: c_slice::Ref<'_, u8>, - out: &mut NewConvoResult, -) { - // Convert input bundle to Introduction - let Ok(intro) = Introduction::try_from(bundle.as_slice()) else { - *out = NewConvoResult { - error_code: ErrorCode::BadIntro as i32, - convo_id: "".into(), - payloads: Vec::new().into(), - }; - return; - }; - - // Create conversation - let (convo_id, payloads) = match ctx.0.create_private_convo(&intro, &content) { - Ok(v) => v, - Err(_) => { - *out = NewConvoResult { - error_code: ErrorCode::UnknownError as i32, - convo_id: "".into(), - payloads: Vec::new().into(), - }; - return; - } - }; - - // Convert payloads to FFI-compatible vector - let ffi_payloads: Vec = payloads - .into_iter() - .map(|p| Payload { - address: p.delivery_address.into(), - data: p.data.into(), - }) - .collect(); - - *out = NewConvoResult { - error_code: 0, - convo_id: convo_id.to_string().into(), - payloads: ffi_payloads.into(), - }; -} - -/// List existing conversations -/// -/// # Returns -/// Returns a struct with conversation ids of available conversations -/// The ListConvoResult must be freed. -#[ffi_export] -pub fn list_conversations(ctx: &mut ContextHandle) -> ListConvoResult { - match ctx.0.list_conversations() { - Ok(ids) => { - let ffi_ids: Vec = - ids.into_iter().map(|id| id.to_string().into()).collect(); - ListConvoResult { - error_code: ErrorCode::None as i32, - convo_ids: ffi_ids.into(), - } - } - Err(_) => ListConvoResult { - error_code: ErrorCode::UnknownError as i32, - convo_ids: repr_c::Vec::EMPTY, - }, - } -} - -/// Sends content to an existing conversation -/// -/// # Returns -/// Returns a PayloadResult with payloads that must be delivered to participants. -/// Check error_code field: 0 means success, negative values indicate errors (see ErrorCode). -/// -/// # ABI note -/// The result is written through `out` (Nim's calling convention for large struct returns). -#[ffi_export] -pub fn send_content( - ctx: &mut ContextHandle, - convo_id: repr_c::String, - content: c_slice::Ref<'_, u8>, - out: &mut SendContentResult, -) { - let payloads = match ctx.0.send_content(&convo_id, &content) { - Ok(p) => p, - Err(_) => { - *out = SendContentResult { - error_code: ErrorCode::UnknownError as i32, - payloads: safer_ffi::Vec::EMPTY, - }; - return; - } - }; - - let ffi_payloads: Vec = payloads - .into_iter() - .map(|p| Payload { - address: p.delivery_address.into(), - data: p.data.into(), - }) - .collect(); - - *out = SendContentResult { - error_code: 0, - payloads: ffi_payloads.into(), - }; -} - -/// Handles an incoming payload -/// -/// # Returns -/// Returns HandlePayloadResult -/// This call does not always generate content. If data is zero bytes long then there -/// is no data, and the converation_id should be ignored. -/// -/// # ABI note -/// The result is written through `out` (Nim's calling convention for large struct returns). -#[ffi_export] -pub fn handle_payload( - ctx: &mut ContextHandle, - payload: c_slice::Ref<'_, u8>, - out: &mut HandlePayloadResult, -) { - *out = match ctx.0.handle_payload(&payload) { - Ok(o) => o.into(), - Err(e) => e.into(), - }; -} - -// ------------------------------------------ -// Return Type Definitions -// ------------------------------------------ - -/// Result structure for create_intro_bundle -/// error_code is 0 on success, negative on error (see ErrorCode) -#[derive_ReprC] -#[repr(C)] -pub struct CreateIntroResult { - pub error_code: i32, - pub intro_bytes: repr_c::Vec, -} - -/// Free the result from create_intro_bundle -/// -/// # ABI note -/// Takes `&mut` instead of ownership because Nim always passes large structs as a pointer; -/// accepting the struct by value would be an ABI mismatch on the caller side. -#[ffi_export] -pub fn destroy_intro_result(result: &mut CreateIntroResult) { - unsafe { std::ptr::drop_in_place(result) } -} - -/// Payload structure for FFI -#[derive(Debug)] -#[derive_ReprC] -#[repr(C)] -pub struct Payload { - pub address: repr_c::String, - pub data: repr_c::Vec, -} - -/// Result structure for send_content -/// error_code is 0 on success, negative on error (see ErrorCode) -#[derive_ReprC] -#[repr(C)] -pub struct SendContentResult { - pub error_code: i32, - pub payloads: repr_c::Vec, -} - -/// Free the result from send_content -/// -/// # ABI note -/// Takes `&mut` instead of ownership because Nim always passes large structs as a pointer; -/// accepting the struct by value would be an ABI mismatch on the caller side. -#[ffi_export] -pub fn destroy_send_content_result(result: &mut SendContentResult) { - unsafe { std::ptr::drop_in_place(result) } -} - -/// Result structure for handle_payload -/// error_code is 0 on success, negative on error (see ErrorCode) -#[derive(Debug)] -#[derive_ReprC] -#[repr(C)] -pub struct HandlePayloadResult { - pub error_code: i32, - pub convo_id: repr_c::String, - pub content: repr_c::Vec, - pub is_new_convo: bool, -} - -/// Free the result from handle_payload -/// -/// # ABI note -/// Takes `&mut` instead of ownership because Nim always passes large structs as a pointer; -/// accepting the struct by value would be an ABI mismatch on the caller side. -#[ffi_export] -pub fn destroy_handle_payload_result(result: &mut HandlePayloadResult) { - unsafe { std::ptr::drop_in_place(result) } -} - -impl From for HandlePayloadResult { - fn from(value: ContentData) -> Self { - HandlePayloadResult { - error_code: ErrorCode::None as i32, - convo_id: value.conversation_id.into(), - content: value.data.into(), - is_new_convo: value.is_new_convo, - } - } -} - -impl From> for HandlePayloadResult { - fn from(value: Option) -> Self { - if let Some(content) = value { - content.into() - } else { - HandlePayloadResult { - error_code: ErrorCode::None as i32, - convo_id: repr_c::String::EMPTY, - content: repr_c::Vec::EMPTY, - is_new_convo: false, - } - } - } -} - -impl From for HandlePayloadResult { - fn from(_value: ChatError) -> Self { - HandlePayloadResult { - // TODO: (P2) Translate ChatError into ErrorCode - error_code: ErrorCode::UnknownError as i32, - convo_id: String::EMPTY, - content: repr_c::Vec::EMPTY, - is_new_convo: false, - } - } -} - -/// Result structure for create_new_private_convo -/// error_code is 0 on success, negative on error (see ErrorCode) -#[derive_ReprC] -#[repr(C)] -pub struct NewConvoResult { - pub error_code: i32, - pub convo_id: repr_c::String, - pub payloads: repr_c::Vec, -} - -/// Free the result from create_new_private_convo -/// -/// # ABI note -/// Takes `&mut` instead of ownership because Nim always passes large structs as a pointer; -/// accepting the struct by value would be an ABI mismatch on the caller side. -#[ffi_export] -pub fn destroy_convo_result(result: &mut NewConvoResult) { - unsafe { std::ptr::drop_in_place(result) } -} - -/// Result structure for create_new_private_convo -/// error_code is 0 on success, negative on error (see ErrorCode) -#[derive_ReprC] -#[repr(C)] -pub struct ListConvoResult { - pub error_code: i32, - pub convo_ids: repr_c::Vec, -} - -/// Free the result from create_new_private_convo -#[ffi_export] -pub fn destroy_list_result(result: ListConvoResult) { - drop(result); -} diff --git a/core/conversations/src/lib.rs b/core/conversations/src/lib.rs index 20a4c0b..bfbb65c 100644 --- a/core/conversations/src/lib.rs +++ b/core/conversations/src/lib.rs @@ -1,4 +1,3 @@ -mod api; mod context; mod conversation; mod crypto; @@ -8,61 +7,6 @@ mod proto; mod types; mod utils; -pub use api::*; pub use context::{Context, Introduction}; pub use errors::ChatError; pub use sqlite::ChatStorage; - -#[cfg(test)] -mod tests { - - use super::*; - - #[test] - fn test_ffi() {} - - #[test] - fn test_message_roundtrip() { - let mut saro = create_context("saro".into()); - let mut raya = create_context("raya".into()); - - // Raya Creates Bundle and Sends to Saro - let mut intro_result = CreateIntroResult { - error_code: -99, - intro_bytes: safer_ffi::Vec::EMPTY, - }; - create_intro_bundle(&mut raya, &mut intro_result); - assert!(is_ok(intro_result.error_code)); - - let raya_bundle = intro_result.intro_bytes.as_ref(); - - // Saro creates a new conversation with Raya - let content: &[u8] = "hello".as_bytes(); - - let mut convo_result = NewConvoResult { - error_code: -99, - convo_id: "".into(), - payloads: safer_ffi::Vec::EMPTY, - }; - create_new_private_convo(&mut saro, raya_bundle, content.into(), &mut convo_result); - assert!(is_ok(convo_result.error_code)); - - // Raya recieves initial message - let payload = convo_result.payloads.first().unwrap(); - - let mut handle_result: HandlePayloadResult = HandlePayloadResult { - error_code: -99, - convo_id: "".into(), - content: safer_ffi::Vec::EMPTY, - is_new_convo: false, - }; - handle_payload(&mut raya, payload.data.as_ref(), &mut handle_result); - assert!(is_ok(handle_result.error_code)); - - // Check that the Content sent was the content received - assert!(handle_result.content.as_ref().as_slice() == content); - - destroy_context(saro); - destroy_context(raya); - } -} diff --git a/double_ratchet.h b/double_ratchet.h deleted file mode 100644 index fd343d1..0000000 --- a/double_ratchet.h +++ /dev/null @@ -1,114 +0,0 @@ -/*! \file */ -/******************************************* - * * - * File auto-generated by `::safer_ffi`. * - * * - * Do not manually edit this file. * - * * - *******************************************/ - -#ifndef __RUST_DOUBLE_RATCHETS__ -#define __RUST_DOUBLE_RATCHETS__ -#ifdef __cplusplus -extern "C" { -#endif - -/** */ -typedef struct FFIRatchetState FFIRatchetState_t; - -/** */ -typedef struct FFIEncryptResult FFIEncryptResult_t; - - -#include -#include - -/** \brief - * Same as [`Vec`][`rust::Vec`], but with guaranteed `#[repr(C)]` layout - */ -typedef struct Vec_uint8 { - /** */ - uint8_t * ptr; - - /** */ - size_t len; - - /** */ - size_t cap; -} Vec_uint8_t; - -/** */ -typedef struct CResult_Vec_uint8_Vec_uint8 { - /** */ - Vec_uint8_t ok; - - /** */ - Vec_uint8_t err; -} CResult_Vec_uint8_Vec_uint8_t; - -/** */ -CResult_Vec_uint8_Vec_uint8_t -double_ratchet_descrypt_message ( - FFIRatchetState_t * state, - FFIEncryptResult_t const * encrypted); - -/** */ -FFIEncryptResult_t * -double_ratchet_encrypt_message ( - FFIRatchetState_t * state, - Vec_uint8_t const * plaintext); - -typedef struct { - uint8_t idx[32]; -} uint8_32_array_t; - -/** */ -typedef struct FFIInstallationKeyPair FFIInstallationKeyPair_t; - -/** */ -FFIRatchetState_t * -double_ratchet_init_receiver ( - uint8_32_array_t shared_secret, - FFIInstallationKeyPair_t const * keypair); - -/** */ -FFIRatchetState_t * -double_ratchet_init_sender ( - uint8_32_array_t shared_secret, - uint8_32_array_t remote_pub); - -/** */ -void -encrypt_result_destroy ( - FFIEncryptResult_t * result); - -/** */ -void -ffi_c_string_free ( - Vec_uint8_t s); - -/** */ -void -installation_key_pair_destroy ( - FFIInstallationKeyPair_t * keypair); - -/** */ -FFIInstallationKeyPair_t * -installation_key_pair_generate (void); - -/** */ -uint8_32_array_t -installation_key_pair_public ( - FFIInstallationKeyPair_t const * keypair); - -/** */ -void -ratchet_state_destroy ( - FFIRatchetState_t * state); - - -#ifdef __cplusplus -} /* extern \"C\" */ -#endif - -#endif /* __RUST_DOUBLE_RATCHETS__ */ diff --git a/nim-bindings/.gitignore b/nim-bindings/.gitignore deleted file mode 100644 index f3685e2..0000000 --- a/nim-bindings/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -nimble.develop -nimble.paths -nimbledeps diff --git a/nim-bindings/README.md b/nim-bindings/README.md deleted file mode 100644 index b97671b..0000000 --- a/nim-bindings/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Nim-bindings - -A Nim wrapping class that exposes LibChat functionality. - - -## Getting Started - -`nimble pingpong` - Run the pingpong example. \ No newline at end of file diff --git a/nim-bindings/bindings.nimble b/nim-bindings/bindings.nimble deleted file mode 100644 index b20ee5f..0000000 --- a/nim-bindings/bindings.nimble +++ /dev/null @@ -1,30 +0,0 @@ -# Package - -version = "0.1.0" -author = "libchat" -description = "Nim Bindings for LibChat" -license = "MIT" -srcDir = "src" -bin = @["libchat"] - - -# Dependencies - -requires "nim >= 2.2.4" -requires "results" - -proc buildRust() = - exec "cargo build --release --manifest-path ../Cargo.toml" - - -# Build Rust library before compiling Nim -before build: - buildRust() - -task pingpong, "Run pingpong example": - buildRust() - exec "nim c -r --path:src --passL:../target/release/liblibchat.a --passL:-lm examples/pingpong.nim" - -task test, "Run comprehensive all-endpoints test": - buildRust() - exec "nim c -r --path:src --passL:../target/release/liblibchat.a --passL:-lm tests/test_all_endpoints.nim" diff --git a/nim-bindings/config.nims b/nim-bindings/config.nims deleted file mode 100644 index 8ee48d2..0000000 --- a/nim-bindings/config.nims +++ /dev/null @@ -1,4 +0,0 @@ -# begin Nimble config (version 2) -when withDir(thisDir(), system.fileExists("nimble.paths")): - include "nimble.paths" -# end Nimble config diff --git a/nim-bindings/examples/pingpong.nim b/nim-bindings/examples/pingpong.nim deleted file mode 100644 index 3c7dfdc..0000000 --- a/nim-bindings/examples/pingpong.nim +++ /dev/null @@ -1,45 +0,0 @@ -import options -import results -import std/strutils - -import ../src/libchat - - -## Convert a string to seq[byte] -proc encode*(s: string): seq[byte] = - if s.len == 0: - return @[] - result = newSeq[byte](s.len) - copyMem(addr result[0], unsafeAddr s[0], s.len) - - -proc pingpong() = - - var raya = newConversationsContext("raya") - var saro = newConversationsContext("saro") - - - # Perform out of band Introduction - let intro = raya.createIntroductionBundle().expect("[Raya] Couldn't create intro bundle") - echo "Raya's Intro Bundle: ",intro - - var (convo_sr, payloads) = saro.createNewPrivateConvo(intro, encode("Hey Raya")).expect("[Saro] Couldn't create convo") - echo "ConvoId:: ", convo_sr - echo "Payload:: ", payloads - - ## Send Payloads to Raya - for p in payloads: - let res = raya.handlePayload(p.data) - if res.isOk: - let opt = res.get() - if opt.isSome: - let content_result = opt.get() - echo "RecvContent: ", content_result.conversationId, " ", content_result.data - else: - echo "Failed to handle payload: ", res.error - - echo "Done" - -when isMainModule: - pingpong() - diff --git a/nim-bindings/src/libchat.nim b/nim-bindings/src/libchat.nim deleted file mode 100644 index 90960c1..0000000 --- a/nim-bindings/src/libchat.nim +++ /dev/null @@ -1,135 +0,0 @@ -import std/options -import std/sequtils -import results -import bindings - -type - LibChat* = object - handle: ContextHandle - buffer_size: int - - PayloadResult* = object - address*: string - data*: seq[uint8] - -## Create a new conversations context -proc newConversationsContext*(name: string): LibChat = - - result.handle = create_context(name.toReprCString) - result.buffer_size = 256 - if result.handle.isNil: - raise newException(IOError, "Failed to create context") - -## Get the friendly name of this context's installation -proc getInstallationName*(ctx: LibChat): string = - if ctx.handle == nil: - return "" - let name = installation_name(ctx.handle) - defer: destroy_string(name) - result = $name - -## Destroy the context and free resources -proc destroy*(ctx: var LibChat) = - - if not ctx.handle.isNil: - destroy_context(ctx.handle) - ctx.handle = nil - -## Helper proc to create buffer of sufficient size -proc getBuffer*(ctx: LibChat): seq[byte] = - newSeq[byte](ctx.buffer_size) - -## Generate a Introduction Bundle -proc createIntroductionBundle*(ctx: LibChat): Result[seq[byte], string] = - if ctx.handle == nil: - return err("Context handle is nil") - - let res = create_intro_bundle(ctx.handle) - defer: destroy_intro_result(res) - - if res.error_code != ErrNone: - return err("Failed to create intro bundle: " & $res.error_code) - - return ok(res.intro_bytes.toSeq()) - -## Create a Private Convo -proc createNewPrivateConvo*(ctx: LibChat, bundle: seq[byte], content: seq[byte]): Result[(string, seq[PayloadResult]), string] = - if ctx.handle == nil: - return err("Context handle is nil") - - if bundle.len == 0: - return err("bundle is zero length") - if content.len == 0: - return err("content is zero length") - - let res = bindings.create_new_private_convo(ctx.handle, bundle.toSlice(), content.toSlice()) - defer: destroy_convo_result(res) - - if res.error_code != 0: - return err("Failed to create private convo: " & $res.error_code) - - var payloads = newSeq[PayloadResult](res.payloads.len) - for i in 0 ..< res.payloads.len: - let p = res.payloads[int(i)] - payloads[int(i)] = PayloadResult(address: $p.address, data: p.data.toSeq()) - - return ok(($res.convo_id, payloads)) - -proc listConversations*(ctx: LibChat): Result[seq[string], string] = - if ctx.handle == nil: - return err("Context handle is nil") - let res = bindings.list_conversations(ctx.handle) - - if res.error_code != 0: - result = err("Failed to list conversations: " & $res.error_code) - destroy_list_result(res) - return - - ok(res.convo_ids.toSeq()) - -## Send content to an existing conversation -proc sendContent*(ctx: LibChat, convoId: string, content: seq[byte]): Result[seq[PayloadResult], string] = - if ctx.handle == nil: - return err("Context handle is nil") - - if content.len == 0: - return err("content is zero length") - - let res = bindings.send_content(ctx.handle, convoId.toReprCString, content.toSlice()) - defer: destroy_send_content_result(res) - - if res.error_code != 0: - return err("Failed to send content: " & $res.error_code) - - let payloads = res.payloads.toSeq().mapIt(PayloadResult(address: $it.address, data: it.data.toSeq())) - return ok(payloads) - -type - ContentResult* = object - conversationId*: string - data*: seq[uint8] - isNewConvo*: bool - -## Handle an incoming payload and decrypt content -proc handlePayload*(ctx: LibChat, payload: seq[byte]): Result[Option[ContentResult], string] = - if ctx.handle == nil: - return err("Context handle is nil") - - if payload.len == 0: - return err("payload is zero length") - - let res = bindings.handle_payload(ctx.handle, payload.toSlice()) - defer: destroy_handle_payload_result(res) - - if res.error_code != ErrNone: - return err("Failed to handle payload: " & $res.error_code) - - let content = res.content.toSeq() - if content.len == 0: - return ok(none(ContentResult)) - - return ok(some(ContentResult( - conversationId: $res.convo_id, - data: content, - isNewConvo: res.is_new_convo - ))) diff --git a/nim-bindings/tests/test_all_endpoints.nim b/nim-bindings/tests/test_all_endpoints.nim deleted file mode 100644 index e9e3f54..0000000 --- a/nim-bindings/tests/test_all_endpoints.nim +++ /dev/null @@ -1,258 +0,0 @@ -# Comprehensive test for all FFI procs declared in bindings.nim. -# -# Design intent: By importing `bindings` directly and calling every importc -# proc at least once, the linker is forced to include ALL symbol references. -# This prevents link-time optimizations from stripping unused symbols and -# catches both link-time crashes (missing symbols) and runtime crashes -# (wrong ABI, segfaults on use). - -import bindings - -# --------------------------------------------------------------------------- -# Assertion helper -# --------------------------------------------------------------------------- - -proc check(cond: bool, msg: string) = - if not cond: - echo "FAIL: ", msg - quit(1) - echo "OK: ", msg - -# --------------------------------------------------------------------------- -# Section 1: Helper proc coverage -# --------------------------------------------------------------------------- - -proc testHelperProcs() = - echo "\n--- testHelperProcs ---" - - # toSlice(string) — non-empty and empty branches - let s = "hello" - let sl = toSlice(s) - check(sl.len == 5, "toSlice(string): correct len") - check(sl.`ptr` != nil, "toSlice(non-empty string): non-nil ptr") - - let emptySl = toSlice("") - check(emptySl.len == 0, "toSlice(empty string): len == 0") - check(emptySl.`ptr` == nil, "toSlice(empty string): ptr == nil") - - # toSlice(seq[byte]) — non-empty and empty branches - let b: seq[byte] = @[0x61'u8, 0x62'u8, 0x63'u8] - let bSl = toSlice(b) - check(bSl.len == 3, "toSlice(seq[byte]): correct len") - check(bSl.`ptr` != nil, "toSlice(non-empty seq[byte]): non-nil ptr") - - let emptyBSl = toSlice(newSeq[byte](0)) - check(emptyBSl.len == 0, "toSlice(empty seq[byte]): len == 0") - check(emptyBSl.`ptr` == nil, "toSlice(empty seq[byte]): ptr == nil") - - # toReprCString(string) and $(ReprCString) round-trip - let name = "testname" - let rcs = toReprCString(name) - check(rcs.len == csize_t(name.len), "toReprCString: correct len") - check(rcs.cap == 0, "toReprCString: cap == 0 (prevents Rust dealloc of Nim memory)") - check(rcs.`ptr` != nil, "toReprCString: non-nil ptr") - check($rcs == name, "$(ReprCString): round-trips to original string") - - let emptyRcs = toReprCString("") - check(emptyRcs.len == 0, "toReprCString(empty): len == 0") - check($emptyRcs == "", "$(empty ReprCString): returns empty string") - - # toBytes(string) - let bs = toBytes("abc") - check(bs.len == 3, "toBytes: correct length") - check(bs[0] == 0x61'u8, "toBytes: correct first byte") - - let emptyBs = toBytes("") - check(emptyBs.len == 0, "toBytes(empty): empty seq") - -# --------------------------------------------------------------------------- -# Section 2: create_context / installation_name / destroy_context -# --------------------------------------------------------------------------- - -proc testContextLifecycle() = - echo "\n--- testContextLifecycle ---" - - let ctx = create_context(toReprCString("lifecycle-test")) - check(ctx != nil, "create_context: returns non-nil handle") - - let iname = installation_name(ctx) - defer: destroy_string(iname) - let inameStr = $iname - check(inameStr.len > 0, "installation_name: returns non-empty name") - echo " installation name: ", inameStr - - destroy_context(ctx) - echo " destroy_context: no crash" - -# --------------------------------------------------------------------------- -# Section 3: Full two-party conversation flow -# --------------------------------------------------------------------------- -# Exercises: create_intro_bundle, create_new_private_convo, handle_payload, -# send_content, and all four destroy_* procs. -# VecPayload helpers ([], len, items) are also exercised here. - -proc testFullConversationFlow() = - echo "\n--- testFullConversationFlow ---" - - let aliceCtx = create_context(toReprCString("alice")) - check(aliceCtx != nil, "Alice: create_context non-nil") - - let bobCtx = create_context(toReprCString("bob")) - check(bobCtx != nil, "Bob: create_context non-nil") - - # --- create_intro_bundle --- - var bobIntroRes = create_intro_bundle(bobCtx) - check(bobIntroRes.error_code == ErrNone, - "create_intro_bundle: error_code == ErrNone") - check(bobIntroRes.intro_bytes.len > 0, - "create_intro_bundle: intro_bytes non-empty") - - # toSeq(VecUint8) - let introBytes = toSeq(bobIntroRes.intro_bytes) - check(introBytes.len > 0, "toSeq(VecUint8): produces non-empty seq") - - # destroy_intro_result - destroy_intro_result(bobIntroRes) - echo " destroy_intro_result: no crash" - - # --- create_new_private_convo --- - var convoRes = create_new_private_convo( - aliceCtx, - toSlice(introBytes), - toSlice("Hello, Bob!") - ) - check(convoRes.error_code == ErrNone, - "create_new_private_convo: error_code == ErrNone") - - let aliceConvoId = $convoRes.convo_id - check(aliceConvoId.len > 0, "create_new_private_convo: convo_id non-empty") - echo " Alice-Bob convo_id: ", aliceConvoId - - # len(VecPayload) - let numPayloads = len(convoRes.payloads) - check(numPayloads > 0, "len(VecPayload): > 0 payloads in new convo") - - # [](VecPayload, int): subscript access - let firstPayload = convoRes.payloads[0] - check(firstPayload.data.len > 0, "VecPayload[0].data: non-empty") - check(firstPayload.address.len > 0, "VecPayload[0].address: non-empty") - echo " first payload address: ", $firstPayload.address - - # items(VecPayload): collect bytes before destroy - var payloadDatas: seq[seq[byte]] = @[] - var iterCount = 0 - for p in convoRes.payloads: - payloadDatas.add(toSeq(p.data)) - inc iterCount - check(iterCount == numPayloads, - "items(VecPayload): iterator yields all payloads") - - # destroy_convo_result - destroy_convo_result(convoRes) - echo " destroy_convo_result: no crash" - - # --- handle_payload --- - var bobSawContent = false - var bobConvoId = "" - for pData in payloadDatas: - var hp = handle_payload(bobCtx, toSlice(pData)) - check(hp.error_code == ErrNone, "handle_payload: error_code == ErrNone") - - let content = toSeq(hp.content) - if content.len > 0: - bobConvoId = $hp.convo_id - check(bobConvoId.len > 0, - "handle_payload: convo_id non-empty when content present") - if not bobSawContent: - check(hp.is_new_convo, - "handle_payload: is_new_convo == true on first contact") - bobSawContent = true - echo " Bob received content in convo: ", bobConvoId - - destroy_handle_payload_result(hp) - - check(bobSawContent, "handle_payload: Bob received Alice's opening message") - echo " destroy_handle_payload_result: no crash" - - # --- send_content --- - var sendRes = send_content( - aliceCtx, - toReprCString(aliceConvoId), - toSlice("How are you, Bob?") - ) - check(sendRes.error_code == ErrNone, - "send_content: error_code == ErrNone for valid convo_id") - check(len(sendRes.payloads) > 0, - "send_content: returns at least one payload") - - var sendPayloadDatas: seq[seq[byte]] = @[] - for p in sendRes.payloads: - sendPayloadDatas.add(toSeq(p.data)) - - # destroy_send_content_result - destroy_send_content_result(sendRes) - echo " destroy_send_content_result: no crash" - - # Bob handles follow-up payloads - for pData in sendPayloadDatas: - var hp2 = handle_payload(bobCtx, toSlice(pData)) - check(hp2.error_code == ErrNone, - "handle_payload: Bob handles send_content payload without error") - destroy_handle_payload_result(hp2) - - destroy_context(aliceCtx) - destroy_context(bobCtx) - echo " both contexts destroyed: no crash" - -# --------------------------------------------------------------------------- -# Section 4: Error-case coverage -# --------------------------------------------------------------------------- -# Exercises destroy_* on error results (empty/null Vecs) to confirm they -# do not crash. - -proc testErrorCases() = - echo "\n--- testErrorCases ---" - - let ctx = create_context(toReprCString("error-tester")) - check(ctx != nil, "error-tester: create_context non-nil") - - # send_content with a nonexistent convo_id must fail - var badSend = send_content( - ctx, - toReprCString("00000000-0000-0000-0000-nonexistent"), - toSlice("payload") - ) - check(badSend.error_code != ErrNone, - "send_content(bad convo_id): error_code != ErrNone") - echo " send_content(bad convo_id) error_code: ", badSend.error_code - # Destroy error result to confirm destroy handles empty VecPayload - destroy_send_content_result(badSend) - echo " destroy_send_content_result(error result): no crash" - - # create_new_private_convo with garbage bytes must fail with ErrBadIntro - let badIntro: seq[byte] = @[0xDE'u8, 0xAD'u8, 0xBE'u8, 0xEF'u8] - var badConvo = create_new_private_convo( - ctx, - toSlice(badIntro), - toSlice("content") - ) - check(badConvo.error_code == ErrBadIntro, - "create_new_private_convo(bad intro): error_code == ErrBadIntro") - destroy_convo_result(badConvo) - echo " destroy_convo_result(error result): no crash" - - destroy_context(ctx) - -# --------------------------------------------------------------------------- -# Entry point -# --------------------------------------------------------------------------- - -when isMainModule: - echo "=== test_all_endpoints: begin ===" - - testHelperProcs() - testContextLifecycle() - testFullConversationFlow() - testErrorCases() - - echo "\n=== ALL TESTS PASSED ==="