mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-04-02 01:23:11 +00:00
Merge df0ffbd2d03fdaeb6832afe3bf18c6824662dcda into 8cddd9ddcfb446deeff96fd5a68d6e4b14927d9f
This commit is contained in:
commit
37fcf5f85a
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
@ -36,23 +36,3 @@ jobs:
|
|||||||
- run: rustup update stable && rustup default stable
|
- run: rustup update stable && rustup default stable
|
||||||
- run: rustup component add rustfmt
|
- run: rustup component add rustfmt
|
||||||
- run: cargo fmt --all -- --check
|
- 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
|
|
||||||
|
|||||||
@ -1,379 +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<u8> : 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 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<ContextHandle> {
|
|
||||||
// Deference name to to `str` and then borrow to &str
|
|
||||||
Box::new(ContextHandle(Context::new_with_name(&*name))).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<ContextHandle>) {
|
|
||||||
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) = ctx.0.create_private_convo(&intro, &content);
|
|
||||||
|
|
||||||
// Convert payloads to FFI-compatible vector
|
|
||||||
let ffi_payloads: Vec<Payload> = 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<repr_c::String> =
|
|
||||||
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<Payload> = 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<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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<Payload>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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<u8>,
|
|
||||||
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<ContentData> 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<Option<ContentData>> for HandlePayloadResult {
|
|
||||||
fn from(value: Option<ContentData>) -> 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<ChatError> 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<Payload>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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<repr_c::String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Free the result from create_new_private_convo
|
|
||||||
#[ffi_export]
|
|
||||||
pub fn destroy_list_result(result: ListConvoResult) {
|
|
||||||
drop(result);
|
|
||||||
}
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
mod api;
|
|
||||||
mod context;
|
mod context;
|
||||||
mod conversation;
|
mod conversation;
|
||||||
mod crypto;
|
mod crypto;
|
||||||
@ -10,60 +9,5 @@ mod storage;
|
|||||||
mod types;
|
mod types;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
pub use api::*;
|
|
||||||
pub use context::{Context, Introduction};
|
pub use context::{Context, Introduction};
|
||||||
pub use errors::ChatError;
|
pub use errors::ChatError;
|
||||||
|
|
||||||
#[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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
114
double_ratchet.h
114
double_ratchet.h
@ -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
|
|
||||||
|
|
||||||
/** <No documentation available> */
|
|
||||||
typedef struct FFIRatchetState FFIRatchetState_t;
|
|
||||||
|
|
||||||
/** <No documentation available> */
|
|
||||||
typedef struct FFIEncryptResult FFIEncryptResult_t;
|
|
||||||
|
|
||||||
|
|
||||||
#include <stddef.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
/** \brief
|
|
||||||
* Same as [`Vec<T>`][`rust::Vec`], but with guaranteed `#[repr(C)]` layout
|
|
||||||
*/
|
|
||||||
typedef struct Vec_uint8 {
|
|
||||||
/** <No documentation available> */
|
|
||||||
uint8_t * ptr;
|
|
||||||
|
|
||||||
/** <No documentation available> */
|
|
||||||
size_t len;
|
|
||||||
|
|
||||||
/** <No documentation available> */
|
|
||||||
size_t cap;
|
|
||||||
} Vec_uint8_t;
|
|
||||||
|
|
||||||
/** <No documentation available> */
|
|
||||||
typedef struct CResult_Vec_uint8_Vec_uint8 {
|
|
||||||
/** <No documentation available> */
|
|
||||||
Vec_uint8_t ok;
|
|
||||||
|
|
||||||
/** <No documentation available> */
|
|
||||||
Vec_uint8_t err;
|
|
||||||
} CResult_Vec_uint8_Vec_uint8_t;
|
|
||||||
|
|
||||||
/** <No documentation available> */
|
|
||||||
CResult_Vec_uint8_Vec_uint8_t
|
|
||||||
double_ratchet_descrypt_message (
|
|
||||||
FFIRatchetState_t * state,
|
|
||||||
FFIEncryptResult_t const * encrypted);
|
|
||||||
|
|
||||||
/** <No documentation available> */
|
|
||||||
FFIEncryptResult_t *
|
|
||||||
double_ratchet_encrypt_message (
|
|
||||||
FFIRatchetState_t * state,
|
|
||||||
Vec_uint8_t const * plaintext);
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
uint8_t idx[32];
|
|
||||||
} uint8_32_array_t;
|
|
||||||
|
|
||||||
/** <No documentation available> */
|
|
||||||
typedef struct FFIInstallationKeyPair FFIInstallationKeyPair_t;
|
|
||||||
|
|
||||||
/** <No documentation available> */
|
|
||||||
FFIRatchetState_t *
|
|
||||||
double_ratchet_init_receiver (
|
|
||||||
uint8_32_array_t shared_secret,
|
|
||||||
FFIInstallationKeyPair_t const * keypair);
|
|
||||||
|
|
||||||
/** <No documentation available> */
|
|
||||||
FFIRatchetState_t *
|
|
||||||
double_ratchet_init_sender (
|
|
||||||
uint8_32_array_t shared_secret,
|
|
||||||
uint8_32_array_t remote_pub);
|
|
||||||
|
|
||||||
/** <No documentation available> */
|
|
||||||
void
|
|
||||||
encrypt_result_destroy (
|
|
||||||
FFIEncryptResult_t * result);
|
|
||||||
|
|
||||||
/** <No documentation available> */
|
|
||||||
void
|
|
||||||
ffi_c_string_free (
|
|
||||||
Vec_uint8_t s);
|
|
||||||
|
|
||||||
/** <No documentation available> */
|
|
||||||
void
|
|
||||||
installation_key_pair_destroy (
|
|
||||||
FFIInstallationKeyPair_t * keypair);
|
|
||||||
|
|
||||||
/** <No documentation available> */
|
|
||||||
FFIInstallationKeyPair_t *
|
|
||||||
installation_key_pair_generate (void);
|
|
||||||
|
|
||||||
/** <No documentation available> */
|
|
||||||
uint8_32_array_t
|
|
||||||
installation_key_pair_public (
|
|
||||||
FFIInstallationKeyPair_t const * keypair);
|
|
||||||
|
|
||||||
/** <No documentation available> */
|
|
||||||
void
|
|
||||||
ratchet_state_destroy (
|
|
||||||
FFIRatchetState_t * state);
|
|
||||||
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
} /* extern \"C\" */
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif /* __RUST_DOUBLE_RATCHETS__ */
|
|
||||||
3
nim-bindings/.gitignore
vendored
3
nim-bindings/.gitignore
vendored
@ -1,3 +0,0 @@
|
|||||||
nimble.develop
|
|
||||||
nimble.paths
|
|
||||||
nimbledeps
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
# Nim-bindings
|
|
||||||
|
|
||||||
A Nim wrapping class that exposes LibChat functionality.
|
|
||||||
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
`nimble pingpong` - Run the pingpong example.
|
|
||||||
@ -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"
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
# begin Nimble config (version 2)
|
|
||||||
when withDir(thisDir(), system.fileExists("nimble.paths")):
|
|
||||||
include "nimble.paths"
|
|
||||||
# end Nimble config
|
|
||||||
@ -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()
|
|
||||||
|
|
||||||
@ -1,232 +0,0 @@
|
|||||||
# Nim FFI bindings for libchat conversations library
|
|
||||||
|
|
||||||
# Error codes (must match Rust ErrorCode enum)
|
|
||||||
const
|
|
||||||
ErrNone* = 0'i32
|
|
||||||
ErrBadPtr* = -1'i32
|
|
||||||
ErrBadConvoId* = -2'i32
|
|
||||||
ErrBadIntro* = -3'i32
|
|
||||||
ErrNotImplemented* = -4'i32
|
|
||||||
ErrBufferExceeded* = -5'i32
|
|
||||||
ErrUnknownError* = -6'i32
|
|
||||||
|
|
||||||
# Opaque handle type for Context
|
|
||||||
type ContextHandle* = pointer
|
|
||||||
|
|
||||||
type
|
|
||||||
## Slice for passing byte arrays to safer_ffi functions
|
|
||||||
SliceUint8* = object
|
|
||||||
`ptr`*: ptr uint8
|
|
||||||
len*: csize_t
|
|
||||||
|
|
||||||
## Vector type returned by safer_ffi functions (must be freed)
|
|
||||||
VecUint8* = object
|
|
||||||
`ptr`*: ptr uint8
|
|
||||||
len*: csize_t
|
|
||||||
cap*: csize_t
|
|
||||||
|
|
||||||
## repr_c::String type from safer_ffi
|
|
||||||
ReprCString* = object
|
|
||||||
`ptr`*: ptr char
|
|
||||||
len*: csize_t
|
|
||||||
cap*: csize_t
|
|
||||||
|
|
||||||
## Payload structure for FFI (matches Rust Payload struct)
|
|
||||||
Payload* = object
|
|
||||||
address*: ReprCString
|
|
||||||
data*: VecUint8
|
|
||||||
|
|
||||||
## Vector of Payloads returned by safer_ffi functions
|
|
||||||
VecPayload* = object
|
|
||||||
`ptr`*: ptr Payload
|
|
||||||
len*: csize_t
|
|
||||||
cap*: csize_t
|
|
||||||
|
|
||||||
## Vector of Payloads returned by safer_ffi functions
|
|
||||||
VecString* = object
|
|
||||||
`ptr`*: ptr ReprCString
|
|
||||||
len*: csize_t
|
|
||||||
cap*: csize_t
|
|
||||||
|
|
||||||
## Result structure for create_intro_bundle
|
|
||||||
## error_code is 0 on success, negative on error (see ErrorCode)
|
|
||||||
CreateIntroResult* = object
|
|
||||||
error_code*: int32
|
|
||||||
intro_bytes*: VecUint8
|
|
||||||
|
|
||||||
## Result structure for send_content
|
|
||||||
## error_code is 0 on success, negative on error (see ErrorCode)
|
|
||||||
SendContentResult* = object
|
|
||||||
error_code*: int32
|
|
||||||
payloads*: VecPayload
|
|
||||||
|
|
||||||
## Result structure for handle_payload
|
|
||||||
## error_code is 0 on success, negative on error (see ErrorCode)
|
|
||||||
HandlePayloadResult* = object
|
|
||||||
error_code*: int32
|
|
||||||
convo_id*: ReprCString
|
|
||||||
content*: VecUint8
|
|
||||||
is_new_convo*: bool
|
|
||||||
|
|
||||||
## Result from create_new_private_convo
|
|
||||||
## error_code is 0 on success, negative on error (see ErrorCode)
|
|
||||||
NewConvoResult* = object
|
|
||||||
error_code*: int32
|
|
||||||
convo_id*: ReprCString
|
|
||||||
payloads*: VecPayload
|
|
||||||
|
|
||||||
## Result from list_conversations
|
|
||||||
## error_code is 0 on success, negative on error (see ErrorCode)
|
|
||||||
ListConvoResult* = object
|
|
||||||
error_code*: int32
|
|
||||||
convo_ids*: VecString
|
|
||||||
|
|
||||||
# FFI function imports
|
|
||||||
|
|
||||||
## Creates a new libchat Context
|
|
||||||
## Returns: Opaque handle to the context. Must be freed with destroy_context()
|
|
||||||
proc create_context*(name: ReprCString): ContextHandle {.importc.}
|
|
||||||
|
|
||||||
## Returns the friendly name of the context's identity
|
|
||||||
## The result must be freed by the caller (repr_c::String ownership transfers)
|
|
||||||
proc installation_name*(ctx: ContextHandle): ReprCString {.importc.}
|
|
||||||
|
|
||||||
## Destroys a context and frees its memory
|
|
||||||
## - handle must be a valid pointer from create_context()
|
|
||||||
## - handle must not be used after this call
|
|
||||||
proc destroy_context*(ctx: ContextHandle) {.importc.}
|
|
||||||
|
|
||||||
## Free a ReprCString returned by any of the FFI functions
|
|
||||||
## - s must be an owned ReprCString value returned from an FFI function
|
|
||||||
## - s must not be used after this call
|
|
||||||
proc destroy_string*(s: ReprCString) {.importc.}
|
|
||||||
|
|
||||||
## Creates an intro bundle for sharing with other users
|
|
||||||
## Returns: CreateIntroResult struct - check error_code field (0 = success, negative = error)
|
|
||||||
## The result must be freed with destroy_intro_result()
|
|
||||||
proc create_intro_bundle*(
|
|
||||||
ctx: ContextHandle,
|
|
||||||
): CreateIntroResult {.importc.}
|
|
||||||
|
|
||||||
## Creates a new private conversation
|
|
||||||
## Returns: NewConvoResult struct - check error_code field (0 = success, negative = error)
|
|
||||||
## The result must be freed with destroy_convo_result()
|
|
||||||
proc create_new_private_convo*(
|
|
||||||
ctx: ContextHandle,
|
|
||||||
bundle: SliceUint8,
|
|
||||||
content: SliceUint8,
|
|
||||||
): NewConvoResult {.importc.}
|
|
||||||
|
|
||||||
## Get the available conversation identifers.
|
|
||||||
## Returns: ListConvoResult struct - check error_code field (0 = success, negative = error)
|
|
||||||
## The result must be freed with destroy_list_result()
|
|
||||||
proc list_conversations*(
|
|
||||||
ctx: ContextHandle,
|
|
||||||
): ListConvoResult {.importc.}
|
|
||||||
|
|
||||||
## Sends content to an existing conversation
|
|
||||||
## Returns: SendContentResult struct - check error_code field (0 = success, negative = error)
|
|
||||||
## The result must be freed with destroy_send_content_result()
|
|
||||||
proc send_content*(
|
|
||||||
ctx: ContextHandle,
|
|
||||||
convo_id: ReprCString,
|
|
||||||
content: SliceUint8,
|
|
||||||
): SendContentResult {.importc.}
|
|
||||||
|
|
||||||
## Handles an incoming payload
|
|
||||||
## Returns: HandlePayloadResult struct - check error_code field (0 = success, negative = error)
|
|
||||||
## This call does not always generate content. If content is zero bytes long then there
|
|
||||||
## is no data, and the convo_id should be ignored.
|
|
||||||
## The result must be freed with destroy_handle_payload_result()
|
|
||||||
proc handle_payload*(
|
|
||||||
ctx: ContextHandle,
|
|
||||||
payload: SliceUint8,
|
|
||||||
): HandlePayloadResult {.importc.}
|
|
||||||
|
|
||||||
## Free the result from create_intro_bundle
|
|
||||||
proc destroy_intro_result*(result: CreateIntroResult) {.importc.}
|
|
||||||
|
|
||||||
## Free the result from create_new_private_convo
|
|
||||||
proc destroy_convo_result*(result: NewConvoResult) {.importc.}
|
|
||||||
|
|
||||||
## Free the result from list_conversation
|
|
||||||
proc destroy_list_result*(result: ListConvoResult) {.importc.}
|
|
||||||
|
|
||||||
## Free the result from send_content
|
|
||||||
proc destroy_send_content_result*(result: SendContentResult) {.importc.}
|
|
||||||
|
|
||||||
## Free the result from handle_payload
|
|
||||||
proc destroy_handle_payload_result*(result: HandlePayloadResult) {.importc.}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Helper functions
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
## Create a SliceRefUint8 from a string
|
|
||||||
proc toSlice*(s: string): SliceUint8 =
|
|
||||||
if s.len == 0:
|
|
||||||
SliceUint8(`ptr`: nil, len: 0)
|
|
||||||
else:
|
|
||||||
SliceUint8(`ptr`: cast[ptr uint8](unsafeAddr s[0]), len: csize_t(s.len))
|
|
||||||
|
|
||||||
## Create a SliceRefUint8 from a seq[byte]
|
|
||||||
proc toSlice*(s: seq[byte]): SliceUint8 =
|
|
||||||
if s.len == 0:
|
|
||||||
SliceUint8(`ptr`: nil, len: 0)
|
|
||||||
else:
|
|
||||||
SliceUint8(`ptr`: cast[ptr uint8](unsafeAddr s[0]), len: csize_t(s.len))
|
|
||||||
|
|
||||||
## Convert a ReprCString to a Nim string
|
|
||||||
proc `$`*(s: ReprCString): string =
|
|
||||||
if s.ptr == nil or s.len == 0:
|
|
||||||
return ""
|
|
||||||
result = newString(s.len)
|
|
||||||
copyMem(addr result[0], s.ptr, s.len)
|
|
||||||
|
|
||||||
## Create a ReprCString from a Nim string for passing to FFI functions.
|
|
||||||
## WARNING: The returned ReprCString borrows from the input string.
|
|
||||||
## The input string must remain valid for the duration of the FFI call.
|
|
||||||
## cap is set to 0 to prevent Rust from attempting to deallocate Nim memory.
|
|
||||||
proc toReprCString*(s: string): ReprCString =
|
|
||||||
if s.len == 0:
|
|
||||||
ReprCString(`ptr`: nil, len: 0, cap: 0)
|
|
||||||
else:
|
|
||||||
ReprCString(`ptr`: cast[ptr char](unsafeAddr s[0]), len: csize_t(s.len), cap: 0)
|
|
||||||
|
|
||||||
## Convert a VecUint8 to a seq[string]
|
|
||||||
proc toSeq*(v: VecString): seq[string] =
|
|
||||||
if v.ptr == nil or v.len == 0:
|
|
||||||
return @[]
|
|
||||||
result = newSeq[string](v.len)
|
|
||||||
let arr = cast[ptr UncheckedArray[ReprCString]](v.ptr)
|
|
||||||
for i in 0 ..< int(v.len):
|
|
||||||
result[i] = $arr[i]
|
|
||||||
|
|
||||||
## Convert a VecUint8 to a seq[byte]
|
|
||||||
proc toSeq*(v: VecUint8): seq[byte] =
|
|
||||||
if v.ptr == nil or v.len == 0:
|
|
||||||
return @[]
|
|
||||||
result = newSeq[byte](v.len)
|
|
||||||
copyMem(addr result[0], v.ptr, v.len)
|
|
||||||
|
|
||||||
## Access payloads from VecPayload
|
|
||||||
proc `[]`*(v: VecPayload, i: int): Payload =
|
|
||||||
assert i >= 0 and csize_t(i) < v.len
|
|
||||||
cast[ptr UncheckedArray[Payload]](v.ptr)[i]
|
|
||||||
|
|
||||||
## Get length of VecPayload
|
|
||||||
proc len*(v: VecPayload): int =
|
|
||||||
int(v.len)
|
|
||||||
|
|
||||||
## Iterator for VecPayload
|
|
||||||
iterator items*(v: VecPayload): Payload =
|
|
||||||
for i in 0 ..< v.len:
|
|
||||||
yield v[int(i)]
|
|
||||||
|
|
||||||
## Convert a string to seq[byte]
|
|
||||||
proc toBytes*(s: string): seq[byte] =
|
|
||||||
if s.len == 0:
|
|
||||||
return @[]
|
|
||||||
result = newSeq[byte](s.len)
|
|
||||||
copyMem(addr result[0], unsafeAddr s[0], s.len)
|
|
||||||
|
|
||||||
@ -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
|
|
||||||
)))
|
|
||||||
@ -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 ==="
|
|
||||||
Loading…
x
Reference in New Issue
Block a user