mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-02-09 16:33:10 +00:00
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:
parent
8e2b5211b4
commit
15bb395475
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -473,6 +473,7 @@ dependencies = [
|
||||
"hex",
|
||||
"prost",
|
||||
"rand_core",
|
||||
"safer-ffi",
|
||||
"thiserror",
|
||||
"x25519-dalek",
|
||||
]
|
||||
|
||||
@ -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"] }
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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:?}")]
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user