From 15bb395475024542d66f33bc50dc8dc4163e1210 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Thu, 29 Jan 2026 00:59:07 +0700 Subject: [PATCH] Rust API - conversation creation (#24) * Adds final changes for rust side API * Add Safer_ffi impl * Simplify api * Add const handle offset --- Cargo.lock | 1 + conversations/Cargo.toml | 1 + conversations/src/api.rs | 245 ++++++++++++++----------------- conversations/src/context.rs | 45 +++++- conversations/src/errors.rs | 2 + conversations/src/inbox/inbox.rs | 6 +- conversations/src/lib.rs | 109 +++----------- 7 files changed, 175 insertions(+), 234 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1970c57..d4f5b92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -473,6 +473,7 @@ dependencies = [ "hex", "prost", "rand_core", + "safer-ffi", "thiserror", "x25519-dalek", ] diff --git a/conversations/Cargo.toml b/conversations/Cargo.toml index 678ee3c..2809a58 100644 --- a/conversations/Cargo.toml +++ b/conversations/Cargo.toml @@ -13,5 +13,6 @@ crypto = { path = "../crypto" } hex = "0.4.3" prost = "0.14.1" rand_core = { version = "0.6" } +safer-ffi = "0.1.13" thiserror = "2.0.17" x25519-dalek = { version = "2.0.1", features = ["static_secrets", "reusable_secrets", "getrandom"] } diff --git a/conversations/src/api.rs b/conversations/src/api.rs index 2e7ed4b..399c45d 100644 --- a/conversations/src/api.rs +++ b/conversations/src/api.rs @@ -1,25 +1,31 @@ -use core::ffi::c_char; -use std::{ffi::CStr, slice}; +use safer_ffi::prelude::*; // Must only contain negative values, 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, } -use crate::context::Context; +use crate::context::{Context, Introduction}; -pub type ContextHandle = *mut Context; +/// 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 conversation_store_destroy() -#[unsafe(no_mangle)] -pub extern "C" fn create_context() -> ContextHandle { - let store = Box::new(Context::new()); - Box::into_raw(store) // Leak the box, return raw pointer +/// Opaque handle to the store. Must be freed with destroy_context() +#[ffi_export] +pub fn create_context() -> repr_c::Box { + Box::new(ContextHandle(Context::new())).into() } /// Destroys a conversation store and frees its memory @@ -28,147 +34,114 @@ pub extern "C" fn create_context() -> ContextHandle { /// - handle must be a valid pointer from conversation_store_create() /// - handle must not be used after this call /// - handle must not be freed twice -#[unsafe(no_mangle)] -pub unsafe extern "C" fn destroy_context(handle: ContextHandle) { - if !handle.is_null() { - unsafe { - let _ = Box::from_raw(handle); // Reconstruct box and drop it - } - } +#[ffi_export] +pub fn destroy_context(ctx: repr_c::Box) { + drop(ctx); } -/// Encrypts/encodes content into payloads. -/// There may be multiple payloads generated from a single content. +/// Creates an intro bundle for sharing with other users /// /// # Returns -/// Returns the number of payloads created. -/// -/// # Errors -/// Negative numbers symbolize an error has occured. See `ErrorCode` -/// -#[unsafe(no_mangle)] -pub unsafe extern "C" fn generate_payload( - // Input: Context Handle - handle: ContextHandle, - // Input: Conversation_id - conversation_id: *const c_char, - // Input: Content array - content: *const u8, - content_len: usize, +/// Returns the number of bytes written to bundle_out +/// Check error_code field: 0 means success, negative values indicate errors (see ErrorCode). +#[ffi_export] +pub fn create_intro_bundle(ctx: &mut ContextHandle, mut bundle_out: c_slice::Mut<'_, u8>) -> i32 { + let Ok(bundle) = ctx.0.create_intro_bundle() else { + return ErrorCode::UnknownError as i32; + }; - max_payload_count: usize, - // Output: Addresses - addrs: *const *mut c_char, - addr_max_len: usize, - - // Output: Frame data - payload_buffer_ptrs: *const *mut u8, - payload_buffer_max_len: *const usize, //Single Value - - // Output: Array - Number of bytes written to each payload - output_actual_lengths: *mut usize, -) -> i32 { - if handle.is_null() || content.is_null() || payload_buffer_ptrs.is_null() || addrs.is_null() { - return ErrorCode::BadPtr as i32; + // Check buffer is large enough + if bundle_out.len() < bundle.len() { + return ErrorCode::BufferExceeded as i32; } - unsafe { - let ctx = &mut *handle; - let content_slice = slice::from_raw_parts(content, content_len); - let payload_ptrs_slice = slice::from_raw_parts(payload_buffer_ptrs, max_payload_count); - let payload_max_len = if !payload_buffer_max_len.is_null() { - *payload_buffer_max_len - } else { - return ErrorCode::BadPtr as i32; - }; - let addrs_slice = slice::from_raw_parts(addrs, max_payload_count); - let actual_lengths_slice = - slice::from_raw_parts_mut(output_actual_lengths, max_payload_count); - - let c_str = CStr::from_ptr(conversation_id); - let id_str = match c_str.to_str() { - Ok(s) => s, - Err(_) => return ErrorCode::BadConvoId as i32, - }; - - // Call ctx.send_content to get payloads - let payloads = ctx.send_content(id_str, content_slice); - - // Check if we have enough output buffers - if payloads.len() > max_payload_count { - return ErrorCode::BadPtr as i32; // Not enough output buffers - } - - // Write each payload to the output buffers - for (i, payload) in payloads.iter().enumerate() { - let payload_ptr = payload_ptrs_slice[i]; - let addr_ptr = addrs_slice[i]; - - // Write payload data - if !payload_ptr.is_null() { - let payload_buf = slice::from_raw_parts_mut(payload_ptr, payload_max_len); - let copy_len = payload.data.len().min(payload_max_len); - payload_buf[..copy_len].copy_from_slice(&payload.data[..copy_len]); - actual_lengths_slice[i] = copy_len; - } else { - return ErrorCode::BadPtr as i32; - } - - // Write delivery address - if !addr_ptr.is_null() { - let addr_bytes = payload.delivery_address.as_bytes(); - let addr_buf = slice::from_raw_parts_mut(addr_ptr as *mut u8, addr_max_len); - let copy_len = addr_bytes.len().min(addr_max_len - 1); - addr_buf[..copy_len].copy_from_slice(&addr_bytes[..copy_len]); - addr_buf[copy_len] = 0; // Null-terminate - } else { - return ErrorCode::BadPtr as i32; - } - } - - payloads.len() as i32 - } + bundle_out[..bundle.len()].copy_from_slice(&bundle); + bundle.len() as i32 } -/// Decrypts/decodes payloads into content. -/// A payload may return 1 or 0 contents. +/// Creates a new private conversation /// /// # Returns -/// Returns the number of bytes written to content -/// -/// # Errors -/// Negative numbers symbolize an error has occured. See `ErrorCode` -/// -#[unsafe(no_mangle)] -pub unsafe extern "C" fn handle_payload( - // Input: Context handle - handle: ContextHandle, - // Input: Payload data - payload_data: *const u8, - payload_len: usize, +/// Returns a struct with payloads that must be sent, the conversation_id that was created. +/// The NewConvoResult must be freed. +#[ffi_export] +pub fn create_new_private_convo( + ctx: &mut ContextHandle, + bundle: c_slice::Ref<'_, u8>, + content: c_slice::Ref<'_, u8>, +) -> NewConvoResult { + // Convert input bundle to Introduction + let s = String::from_utf8_lossy(&bundle).to_string(); + let Ok(intro) = Introduction::try_from(s) else { + return NewConvoResult { + error_code: ErrorCode::BadIntro as i32, + convo_id: 0, + payloads: Vec::new().into(), + }; + }; - // Output: Content - content: *mut u8, - content_max_len: usize, -) -> i32 { - if handle.is_null() || payload_data.is_null() || content.is_null() { - return ErrorCode::BadPtr as i32; - } + // Convert input content to String + let msg = String::from_utf8_lossy(&content).into_owned(); - unsafe { - let ctx = &mut *handle; - let payload_slice = slice::from_raw_parts(payload_data, payload_len); - let content_buf = slice::from_raw_parts_mut(content, content_max_len); + // Create conversation + let (convo_handle, payloads) = ctx.0.create_private_convo(&intro, msg); - // Call ctx.handle_payload to decode the payload - let contents = ctx.handle_payload(payload_slice); + // 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(); - if let Some(content_data) = contents { - let copy_len = content_data.data.len().min(content_max_len); - content_buf[..copy_len].copy_from_slice(&content_data.data[..copy_len]); - copy_len as i32 - } else { - 0 // No content produced - } + NewConvoResult { + error_code: 0, + convo_id: convo_handle, + payloads: ffi_payloads.into(), } } + +// ============================================================================ +// safer_ffi implementation +// =============================================================================================================================== + +/// 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 create_intro_bundle_safe +/// error_code is 0 on success, negative on error (see ErrorCode) +#[derive_ReprC] +#[repr(C)] +pub struct PayloadResult { + pub error_code: i32, + pub payloads: repr_c::Vec, +} + +/// Free the result from create_intro_bundle_safe +#[ffi_export] +pub fn destroy_payload_result(result: PayloadResult) { + drop(result); +} + +/// Result structure for create_new_private_convo_safe +/// 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: u32, + pub payloads: repr_c::Vec, +} + +/// Free the result from create_new_private_convo_safe +#[ffi_export] +pub fn destroy_convo_result(result: NewConvoResult) { + drop(result); +} diff --git a/conversations/src/context.rs b/conversations/src/context.rs index e40f97c..75ced2f 100644 --- a/conversations/src/context.rs +++ b/conversations/src/context.rs @@ -1,20 +1,30 @@ use std::{collections::HashMap, rc::Rc, sync::Arc}; use crate::{ - conversation::{ConversationId, ConversationIdOwned, ConversationStore}, + conversation::{ConversationId, ConversationStore, Convo, Id}, + errors::ChatError, identity::Identity, inbox::Inbox, - proto, types::{ContentData, PayloadData}, }; pub use crate::inbox::Introduction; + +//Offset handles to make debuging easier +const INITIAL_CONVO_HANDLE: u32 = 0xF5000001; + +/// Used to identify a conversation on the othersize of the FFI. +type ConvoHandle = u32; + // This is the main entry point to the conversations api. // Ctx manages lifetimes of objects to process and generate payloads. pub struct Context { _identity: Rc, store: ConversationStore, inbox: Inbox, + buf_size: usize, + convo_handle_map: HashMap>, + next_convo_handle: ConvoHandle, } impl Context { @@ -25,21 +35,32 @@ impl Context { _identity: identity, store: ConversationStore::new(), inbox, + buf_size: 0, + convo_handle_map: HashMap::new(), + next_convo_handle: INITIAL_CONVO_HANDLE, } } + pub fn buffer_size(&self) -> usize { + self.buf_size + } + + pub fn set_buffer_size(&mut self, size: usize) { + self.buf_size = size + } + pub fn create_private_convo( &mut self, remote_bundle: &Introduction, content: String, - ) -> (ConversationIdOwned, Vec) { + ) -> (ConvoHandle, Vec) { let (convo, payloads) = self .inbox .invite_to_private_convo(remote_bundle, content) .unwrap_or_else(|_| todo!("Log/Surface Error")); - let convo_id = self.store.insert_convo(convo); - (convo_id, payloads) + let convo_handle = self.add_convo(convo); + (convo_handle, payloads) } pub fn send_content(&mut self, _convo_id: ConversationId, _content: &[u8]) -> Vec { @@ -57,6 +78,20 @@ impl Context { data: vec![1, 2, 3, 4, 5, 6], }) } + + pub fn create_intro_bundle(&mut self) -> Result, ChatError> { + let pkb = self.inbox.create_bundle(); + Ok(Introduction::from(pkb).into()) + } + + fn add_convo(&mut self, convo: impl Convo + Id + 'static) -> ConvoHandle { + let handle = self.next_convo_handle; + self.next_convo_handle += 1; + let convo_id = self.store.insert_convo(convo); + self.convo_handle_map.insert(handle, convo_id); + + handle + } } #[cfg(test)] diff --git a/conversations/src/errors.rs b/conversations/src/errors.rs index d6c82b6..fc57a8b 100644 --- a/conversations/src/errors.rs +++ b/conversations/src/errors.rs @@ -4,6 +4,8 @@ pub use thiserror::Error; pub enum ChatError { #[error("protocol error: {0:?}")] Protocol(String), + #[error("protocol error: Got {0:?} expected {1:?}")] + ProtocolExpectation(&'static str, String), #[error("Failed to decode payload: {0}")] DecodeError(#[from] prost::DecodeError), #[error("incorrect bundle value: {0:?}")] diff --git a/conversations/src/inbox/inbox.rs b/conversations/src/inbox/inbox.rs index 7853b6d..b9febef 100644 --- a/conversations/src/inbox/inbox.rs +++ b/conversations/src/inbox/inbox.rs @@ -182,9 +182,9 @@ impl Inbox { payload: proto::EncryptedPayload, ) -> Result { let Some(proto::Encryption::InboxHandshake(handshake)) = payload.encryption else { - return Err(ChatError::Protocol( - "Expected inboxhandshake encryption".into(), - )); + let got = format!("{:?}", payload.encryption); + + return Err(ChatError::ProtocolExpectation("inboxhandshake", got)); }; Ok(handshake) diff --git a/conversations/src/lib.rs b/conversations/src/lib.rs index a94ca3e..ed1a271 100644 --- a/conversations/src/lib.rs +++ b/conversations/src/lib.rs @@ -15,104 +15,33 @@ pub use api::*; mod tests { use super::*; - use std::ffi::CString; + use std::str::FromStr; #[test] fn test_ffi() {} #[test] - fn test_process_and_write() { - // Create Context - let ctx = create_context(); - - // Setup conversation_id - let conv_id = CString::new("test_conversation_123").unwrap(); - - // Setup content - let content = b"Hello, World!"; - - // Setup output buffers for addresses (labels) - let addr_max_len = 256; - let mut addr_buffer1: Vec = vec![0; addr_max_len]; - let mut addr_buffer2: Vec = vec![0; addr_max_len]; - - let addr_ptrs: Vec<*mut i8> = vec![ - addr_buffer1.as_mut_ptr() as *mut i8, - addr_buffer2.as_mut_ptr() as *mut i8, - ]; - - // Setup payload buffers - let max_payload_count = 2; - let payload_max_len = 1024; - let mut payload1: Vec = vec![0; payload_max_len]; - let mut payload2: Vec = vec![0; payload_max_len]; - - let payload_ptrs: Vec<*mut u8> = vec![payload1.as_mut_ptr(), payload2.as_mut_ptr()]; - - let payload_max_lens: Vec = vec![payload_max_len, payload_max_len]; - let mut actual_lengths: Vec = vec![0; max_payload_count]; - - // Call the FFI function - let result = unsafe { - generate_payload( - ctx, - conv_id.as_ptr(), - content.as_ptr(), - content.len(), - max_payload_count, - addr_ptrs.as_ptr(), - addr_max_len, - payload_ptrs.as_ptr(), - payload_max_lens.as_ptr(), - actual_lengths.as_mut_ptr(), - ) - }; - - // Verify results - assert_eq!(result, 1, "Function should return 1 on success"); - - // Check that the conversation ID was written to the first label buffer - let written_addr = std::ffi::CStr::from_bytes_until_nul(&addr_buffer1) - .unwrap() - .to_str() - .unwrap(); - - assert_eq!(written_addr, "test_conversation_123"); + fn test_invite_convo() { + let mut ctx = create_context(); + let mut bundle = vec![0u8; 200]; + let bundle_len = create_intro_bundle(&mut ctx, (&mut bundle[..]).into()); unsafe { - destroy_context(ctx); - } - } - - #[test] - fn test_process_and_write_null_ptr() { - use std::ptr; - // Create Context - let ctx = create_context(); - - let conv_id = CString::new("test").unwrap(); - let content = b"test"; - - // Test with null content pointer - let result = unsafe { - generate_payload( - ctx, - conv_id.as_ptr(), - ptr::null(), - content.len(), - 1, - ptr::null(), - 256, - ptr::null(), - ptr::null(), - ptr::null_mut(), - ) - }; - - unsafe { - destroy_context(ctx); + bundle.set_len(bundle_len as usize); } - assert_eq!(result, -1, "Should return ERR_BAD_PTR for null pointer"); + assert!(bundle_len > 0, "bundle failed: {}", bundle_len); + let content = String::from_str("Hello").unwrap(); + let result = create_new_private_convo( + &mut ctx, + bundle.as_slice().into(), + content.as_bytes().into(), + ); + + assert!(result.error_code == 0, "Error: {}", result.error_code); + + println!(" ID:{:?} Payloads:{:?}", result.convo_id, result.payloads); + + destroy_context(ctx); } }