Rust API - conversation creation (#24)

* Adds final changes for rust side API

* Add Safer_ffi impl

* Simplify api

* Add const handle offset
This commit is contained in:
Jazz Turner-Baggs 2026-01-29 00:59:07 +07:00 committed by GitHub
parent 8e2b5211b4
commit 15bb395475
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 175 additions and 234 deletions

1
Cargo.lock generated
View File

@ -473,6 +473,7 @@ dependencies = [
"hex",
"prost",
"rand_core",
"safer-ffi",
"thiserror",
"x25519-dalek",
]

View File

@ -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"] }

View File

@ -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<ContextHandle> {
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<ContextHandle>) {
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<Payload> = 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<u8>,
}
/// 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<Payload>,
}
/// 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<Payload>,
}
/// Free the result from create_new_private_convo_safe
#[ffi_export]
pub fn destroy_convo_result(result: NewConvoResult) {
drop(result);
}

View File

@ -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<Identity>,
store: ConversationStore,
inbox: Inbox,
buf_size: usize,
convo_handle_map: HashMap<u32, Arc<str>>,
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<PayloadData>) {
) -> (ConvoHandle, Vec<PayloadData>) {
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<PayloadData> {
@ -57,6 +78,20 @@ impl Context {
data: vec![1, 2, 3, 4, 5, 6],
})
}
pub fn create_intro_bundle(&mut self) -> Result<Vec<u8>, 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)]

View File

@ -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:?}")]

View File

@ -182,9 +182,9 @@ impl Inbox {
payload: proto::EncryptedPayload,
) -> Result<proto::InboxHandshakeV1, ChatError> {
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)

View File

@ -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<u8> = vec![0; addr_max_len];
let mut addr_buffer2: Vec<u8> = 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<u8> = vec![0; payload_max_len];
let mut payload2: Vec<u8> = 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<usize> = vec![payload_max_len, payload_max_len];
let mut actual_lengths: Vec<usize> = 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);
}
}