mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-02-10 08:53:08 +00:00
Merge branch 'main' of github.com:logos-messaging/libchat into storage-crate
This commit is contained in:
commit
5626dad62a
3
.gitignore
vendored
3
.gitignore
vendored
@ -24,6 +24,9 @@ target
|
||||
|
||||
# Compiled binary
|
||||
**/ffi_nim_example
|
||||
/nim-bindings/examples/pingpong
|
||||
/nim-bindings/libchat
|
||||
|
||||
# Temporary data folder
|
||||
tmp
|
||||
|
||||
|
||||
14
Cargo.lock
generated
14
Cargo.lock
generated
@ -222,6 +222,7 @@ dependencies = [
|
||||
"rand",
|
||||
"rand_core",
|
||||
"safer-ffi",
|
||||
"serde",
|
||||
"storage",
|
||||
"thiserror",
|
||||
"x25519-dalek",
|
||||
@ -473,6 +474,7 @@ dependencies = [
|
||||
"hex",
|
||||
"prost",
|
||||
"rand_core",
|
||||
"safer-ffi",
|
||||
"thiserror",
|
||||
"x25519-dalek",
|
||||
]
|
||||
@ -507,9 +509,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-src"
|
||||
version = "300.5.4+3.5.4"
|
||||
version = "300.5.5+3.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72"
|
||||
checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
@ -1045,18 +1047,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.34"
|
||||
version = "0.8.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d"
|
||||
checksum = "fdea86ddd5568519879b8187e1cf04e24fce28f7fe046ceecbce472ff19a2572"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.34"
|
||||
version = "0.8.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d"
|
||||
checksum = "0c15e1b46eff7c6c91195752e0eeed8ef040e391cdece7c25376957d5f15df22"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@ -4,7 +4,7 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib"]
|
||||
crate-type = ["staticlib","dylib"]
|
||||
|
||||
[dependencies]
|
||||
blake2.workspace = true
|
||||
@ -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, ConvoHandle, 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,176 @@ 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 Ok(intro) = Introduction::try_from(bundle.as_slice()) 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(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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).
|
||||
#[ffi_export]
|
||||
pub fn send_content(
|
||||
ctx: &mut ContextHandle,
|
||||
convo_handle: ConvoHandle,
|
||||
content: c_slice::Ref<'_, u8>,
|
||||
) -> PayloadResult {
|
||||
let payloads = ctx.0.send_content(convo_handle, &content);
|
||||
|
||||
let ffi_payloads: Vec<Payload> = payloads
|
||||
.into_iter()
|
||||
.map(|p| Payload {
|
||||
address: p.delivery_address.into(),
|
||||
data: p.data.into(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
PayloadResult {
|
||||
error_code: 0,
|
||||
payloads: ffi_payloads.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles an incoming payload and writes content to caller-provided buffers
|
||||
///
|
||||
/// # Returns
|
||||
/// Returns the number of bytes written to data_out on success (>= 0).
|
||||
/// Returns negative error code on failure (see ErrorCode).
|
||||
/// conversation_id_out_len is set to the number of bytes written to conversation_id_out.
|
||||
#[ffi_export]
|
||||
pub fn handle_payload(
|
||||
ctx: &mut ContextHandle,
|
||||
payload: c_slice::Ref<'_, u8>,
|
||||
mut conversation_id_out: c_slice::Mut<'_, u8>,
|
||||
conversation_id_out_len: Out<'_, u32>,
|
||||
mut content_out: c_slice::Mut<'_, u8>,
|
||||
) -> i32 {
|
||||
match ctx.0.handle_payload(&payload) {
|
||||
Some(content) => {
|
||||
let convo_id_bytes = content.conversation_id.as_bytes();
|
||||
|
||||
if conversation_id_out.len() < convo_id_bytes.len() {
|
||||
return ErrorCode::BufferExceeded as i32;
|
||||
}
|
||||
|
||||
if content_out.len() < content.data.len() {
|
||||
return ErrorCode::BufferExceeded as i32;
|
||||
}
|
||||
|
||||
conversation_id_out[..convo_id_bytes.len()].copy_from_slice(convo_id_bytes);
|
||||
conversation_id_out_len.write(convo_id_bytes.len() as u32);
|
||||
content_out[..content.data.len()].copy_from_slice(&content.data);
|
||||
|
||||
content.data.len() as i32
|
||||
}
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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.
|
||||
pub 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,27 +35,38 @@ 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> {
|
||||
pub fn send_content(&mut self, convo_id: ConvoHandle, _content: &[u8]) -> Vec<PayloadData> {
|
||||
// !TODO Replace Mock
|
||||
vec![PayloadData {
|
||||
delivery_address: _convo_id.into(),
|
||||
delivery_address: format!("addr-for-{convo_id}"),
|
||||
data: vec![40, 30, 20, 10],
|
||||
}]
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -31,12 +31,11 @@ impl Into<Vec<u8>> for Introduction {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<u8>> for Introduction {
|
||||
impl TryFrom<&[u8]> for Introduction {
|
||||
type Error = ChatError;
|
||||
|
||||
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
|
||||
let str_value =
|
||||
String::from_utf8(value).map_err(|_| ChatError::BadParsing("Introduction"))?;
|
||||
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||
let str_value = String::from_utf8_lossy(value);
|
||||
let parts: Vec<&str> = str_value.splitn(3, ':').collect();
|
||||
|
||||
if parts[0] != "Bundle" {
|
||||
|
||||
@ -15,104 +15,79 @@ 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);
|
||||
bundle.set_len(bundle_len as usize);
|
||||
}
|
||||
|
||||
assert!(bundle_len > 0, "bundle failed: {}", bundle_len);
|
||||
let content = b"Hello";
|
||||
let result = create_new_private_convo(&mut ctx, bundle[..].into(), content[..].into());
|
||||
|
||||
assert!(result.error_code == 0, "Error: {}", result.error_code);
|
||||
|
||||
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(),
|
||||
)
|
||||
};
|
||||
fn test_message_roundtrip() {
|
||||
let mut saro = create_context();
|
||||
let mut raya = create_context();
|
||||
let mut raya_bundle = vec![0u8; 200];
|
||||
|
||||
let bundle_len = create_intro_bundle(&mut raya, (&mut raya_bundle[..]).into());
|
||||
unsafe {
|
||||
destroy_context(ctx);
|
||||
raya_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 saro,
|
||||
raya_bundle.as_slice().into(),
|
||||
content.as_bytes().into(),
|
||||
);
|
||||
|
||||
assert!(result.error_code == 0, "Error: {}", result.error_code);
|
||||
|
||||
// Handle payloads on raya's side
|
||||
let mut conversation_id_out = vec![0u8; 256];
|
||||
let mut conversation_id_out_len: u32 = 0;
|
||||
let mut content_out = vec![0u8; 256];
|
||||
|
||||
for p in result.payloads.iter() {
|
||||
let bytes_written = handle_payload(
|
||||
&mut raya,
|
||||
p.data[..].into(),
|
||||
(&mut conversation_id_out[..]).into(),
|
||||
(&mut conversation_id_out_len).into(),
|
||||
(&mut content_out[..]).into(),
|
||||
);
|
||||
|
||||
unsafe {
|
||||
content_out.set_len(bytes_written as usize);
|
||||
}
|
||||
|
||||
assert!(
|
||||
bytes_written >= 0,
|
||||
"handle_payload failed: {}",
|
||||
bytes_written
|
||||
);
|
||||
|
||||
//TODO: Verify output match
|
||||
}
|
||||
|
||||
destroy_context(saro);
|
||||
destroy_context(raya);
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ blake2 = "0.10.6"
|
||||
safer-ffi = "0.1.13"
|
||||
zeroize = "1.8.2"
|
||||
storage = { workspace = true }
|
||||
serde = "1.0"
|
||||
|
||||
[features]
|
||||
headers = ["safer-ffi/headers"]
|
||||
|
||||
75
double-ratchets/examples/serialization_demo.rs
Normal file
75
double-ratchets/examples/serialization_demo.rs
Normal file
@ -0,0 +1,75 @@
|
||||
use double_ratchets::{InstallationKeyPair, RatchetState, hkdf::PrivateV1Domain};
|
||||
|
||||
fn main() {
|
||||
// === Initial shared secret (X3DH / prekey result in real systems) ===
|
||||
let shared_secret = [42u8; 32];
|
||||
|
||||
let bob_dh = InstallationKeyPair::generate();
|
||||
|
||||
let mut alice: RatchetState<PrivateV1Domain> =
|
||||
RatchetState::init_sender(shared_secret, bob_dh.public().clone());
|
||||
let mut bob: RatchetState<PrivateV1Domain> = RatchetState::init_receiver(shared_secret, bob_dh);
|
||||
|
||||
let (ciphertext, header) = alice.encrypt_message(b"Hello Bob!");
|
||||
|
||||
// === Bob receives ===
|
||||
let plaintext = bob.decrypt_message(&ciphertext, header);
|
||||
println!(
|
||||
"Bob received: {}",
|
||||
String::from_utf8_lossy(&plaintext.unwrap())
|
||||
);
|
||||
|
||||
// === Bob replies (triggers DH ratchet) ===
|
||||
let (ciphertext, header) = bob.encrypt_message(b"Hi Alice!");
|
||||
|
||||
let plaintext = alice.decrypt_message(&ciphertext, header);
|
||||
println!(
|
||||
"Alice received: {}",
|
||||
String::from_utf8_lossy(&plaintext.unwrap())
|
||||
);
|
||||
|
||||
// === Serialize the state of alice and bob ===
|
||||
println!("Before restart, persist the state");
|
||||
let alice_state = alice.as_bytes();
|
||||
let bob_state = bob.as_bytes();
|
||||
|
||||
// === Deserialize alice and bob state from bytes ===
|
||||
println!("Restart alice and bob");
|
||||
let mut alice_new: RatchetState<PrivateV1Domain> =
|
||||
RatchetState::from_bytes(&alice_state).unwrap();
|
||||
let mut bob_new: RatchetState<PrivateV1Domain> = RatchetState::from_bytes(&bob_state).unwrap();
|
||||
|
||||
// === Alice sends a message ===
|
||||
let (ciphertext, header) = alice_new.encrypt_message(b"Hello Bob!");
|
||||
|
||||
// === Bob receives ===
|
||||
let plaintext = bob_new.decrypt_message(&ciphertext, header);
|
||||
println!(
|
||||
"New Bob received: {}",
|
||||
String::from_utf8_lossy(&plaintext.unwrap())
|
||||
);
|
||||
|
||||
// === Bob replies (triggers DH ratchet) ===
|
||||
let (ciphertext, header) = bob_new.encrypt_message(b"Hi Alice!");
|
||||
|
||||
let plaintext = alice_new.decrypt_message(&ciphertext, header);
|
||||
println!(
|
||||
"New Alice received: {}",
|
||||
String::from_utf8_lossy(&plaintext.unwrap())
|
||||
);
|
||||
|
||||
let (skipped_ciphertext, skipped_header) = bob_new.encrypt_message(b"Hi Alice skipped!");
|
||||
let (resumed_ciphertext, resumed_header) = bob_new.encrypt_message(b"Hi Alice resumed!");
|
||||
|
||||
let plaintext = alice_new.decrypt_message(&resumed_ciphertext, resumed_header);
|
||||
println!(
|
||||
"New Alice received: {}",
|
||||
String::from_utf8_lossy(&plaintext.unwrap())
|
||||
);
|
||||
|
||||
let plaintext = alice_new.decrypt_message(&skipped_ciphertext, skipped_header);
|
||||
println!(
|
||||
"New Alice received: {}",
|
||||
String::from_utf8_lossy(&plaintext.unwrap())
|
||||
);
|
||||
}
|
||||
@ -23,4 +23,7 @@ pub enum RatchetError {
|
||||
|
||||
#[error("missing receiving chain")]
|
||||
MissingReceivingChain,
|
||||
|
||||
#[error("deserialization failed")]
|
||||
DeserializationFailed,
|
||||
}
|
||||
|
||||
@ -25,12 +25,12 @@ impl InstallationKeyPair {
|
||||
&self.public
|
||||
}
|
||||
|
||||
/// Export the secret key as raw bytes for storage.
|
||||
pub fn secret_bytes(&self) -> [u8; 32] {
|
||||
self.secret.to_bytes()
|
||||
/// Export the secret key as raw bytes for serialization/storage.
|
||||
pub fn secret_bytes(&self) -> &[u8; 32] {
|
||||
self.secret.as_bytes()
|
||||
}
|
||||
|
||||
/// Reconstruct from secret key bytes.
|
||||
/// Import the secret key from raw bytes.
|
||||
pub fn from_secret_bytes(bytes: [u8; 32]) -> Self {
|
||||
let secret = StaticSecret::from(bytes);
|
||||
let public = PublicKey::from(&secret);
|
||||
|
||||
@ -3,6 +3,7 @@ pub mod errors;
|
||||
pub mod ffi;
|
||||
pub mod hkdf;
|
||||
pub mod keypair;
|
||||
pub mod reader;
|
||||
pub mod state;
|
||||
pub mod storage;
|
||||
pub mod types;
|
||||
|
||||
135
double-ratchets/src/reader.rs
Normal file
135
double-ratchets/src/reader.rs
Normal file
@ -0,0 +1,135 @@
|
||||
use crate::errors::RatchetError;
|
||||
|
||||
pub struct Reader<'a> {
|
||||
data: &'a [u8],
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<'a> Reader<'a> {
|
||||
pub fn new(data: &'a [u8]) -> Self {
|
||||
Self { data, pos: 0 }
|
||||
}
|
||||
|
||||
pub fn read_bytes(&mut self, n: usize) -> Result<&[u8], RatchetError> {
|
||||
if self.pos + n > self.data.len() {
|
||||
return Err(RatchetError::DeserializationFailed);
|
||||
}
|
||||
let slice = &self.data[self.pos..self.pos + n];
|
||||
self.pos += n;
|
||||
Ok(slice)
|
||||
}
|
||||
|
||||
pub fn read_array<const N: usize>(&mut self) -> Result<[u8; N], RatchetError> {
|
||||
self.read_bytes(N)?
|
||||
.try_into()
|
||||
.map_err(|_| RatchetError::DeserializationFailed)
|
||||
}
|
||||
|
||||
pub fn read_u8(&mut self) -> Result<u8, RatchetError> {
|
||||
Ok(self.read_bytes(1)?[0])
|
||||
}
|
||||
|
||||
pub fn read_u32(&mut self) -> Result<u32, RatchetError> {
|
||||
Ok(u32::from_be_bytes(self.read_array()?))
|
||||
}
|
||||
|
||||
pub fn read_option(&mut self) -> Result<Option<[u8; 32]>, RatchetError> {
|
||||
match self.read_u8()? {
|
||||
0x00 => Ok(None),
|
||||
0x01 => Ok(Some(self.read_array()?)),
|
||||
_ => Err(RatchetError::DeserializationFailed),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_read_bytes() {
|
||||
let data = [1, 2, 3, 4, 5];
|
||||
let mut reader = Reader::new(&data);
|
||||
|
||||
assert_eq!(reader.read_bytes(2).unwrap(), &[1, 2]);
|
||||
assert_eq!(reader.read_bytes(3).unwrap(), &[3, 4, 5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_bytes_overflow() {
|
||||
let data = [1, 2, 3];
|
||||
let mut reader = Reader::new(&data);
|
||||
|
||||
assert!(matches!(
|
||||
reader.read_bytes(4),
|
||||
Err(RatchetError::DeserializationFailed)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_array() {
|
||||
let data = [1, 2, 3, 4];
|
||||
let mut reader = Reader::new(&data);
|
||||
|
||||
let arr: [u8; 4] = reader.read_array().unwrap();
|
||||
assert_eq!(arr, [1, 2, 3, 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_u8() {
|
||||
let data = [0x42, 0xFF];
|
||||
let mut reader = Reader::new(&data);
|
||||
|
||||
assert_eq!(reader.read_u8().unwrap(), 0x42);
|
||||
assert_eq!(reader.read_u8().unwrap(), 0xFF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_u32() {
|
||||
let data = [0x00, 0x01, 0x02, 0x03];
|
||||
let mut reader = Reader::new(&data);
|
||||
|
||||
assert_eq!(reader.read_u32().unwrap(), 0x00010203);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_option_none() {
|
||||
let data = [0x00];
|
||||
let mut reader = Reader::new(&data);
|
||||
|
||||
assert_eq!(reader.read_option().unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_option_some() {
|
||||
let mut data = vec![0x01];
|
||||
data.extend_from_slice(&[0x42; 32]);
|
||||
let mut reader = Reader::new(&data);
|
||||
|
||||
assert_eq!(reader.read_option().unwrap(), Some([0x42; 32]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_option_invalid_flag() {
|
||||
let data = [0x02];
|
||||
let mut reader = Reader::new(&data);
|
||||
|
||||
assert!(matches!(
|
||||
reader.read_option(),
|
||||
Err(RatchetError::DeserializationFailed)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sequential_reads() {
|
||||
let mut data = vec![0x01]; // version
|
||||
data.extend_from_slice(&[0xAA; 32]); // 32-byte array
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x00, 0x10]); // u32 = 16
|
||||
|
||||
let mut reader = Reader::new(&data);
|
||||
|
||||
assert_eq!(reader.read_u8().unwrap(), 0x01);
|
||||
assert_eq!(reader.read_array::<32>().unwrap(), [0xAA; 32]);
|
||||
assert_eq!(reader.read_u32().unwrap(), 16);
|
||||
}
|
||||
}
|
||||
@ -1,15 +1,21 @@
|
||||
use std::{collections::HashMap, marker::PhantomData};
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as DeError};
|
||||
use x25519_dalek::PublicKey;
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
|
||||
use crate::{
|
||||
aead::{decrypt, encrypt},
|
||||
errors::RatchetError,
|
||||
hkdf::{DefaultDomain, HkdfInfo, kdf_chain, kdf_root},
|
||||
keypair::InstallationKeyPair,
|
||||
reader::Reader,
|
||||
types::{ChainKey, MessageKey, Nonce, RootKey, SharedSecret},
|
||||
};
|
||||
|
||||
/// Current binary format version.
|
||||
const SERIALIZATION_VERSION: u8 = 1;
|
||||
|
||||
/// Represents the local state of the Double Ratchet algorithm for one conversation.
|
||||
///
|
||||
/// This struct maintains all keys and counters required to perform the Double Ratchet
|
||||
@ -42,6 +48,153 @@ pub struct SkippedKey {
|
||||
pub message_key: MessageKey,
|
||||
}
|
||||
|
||||
impl<D: HkdfInfo> RatchetState<D> {
|
||||
/// Serializes the ratchet state to a binary format.
|
||||
///
|
||||
/// # Binary Format (Version 1)
|
||||
///
|
||||
/// ```text
|
||||
/// | Field | Size (bytes) | Description |
|
||||
/// |--------------------|--------------|--------------------------------------|
|
||||
/// | version | 1 | Format version (0x01) |
|
||||
/// | root_key | 32 | Root key |
|
||||
/// | sending_chain_flag | 1 | 0x00 = None, 0x01 = Some |
|
||||
/// | sending_chain | 0 or 32 | Chain key if flag is 0x01 |
|
||||
/// | receiving_chain_flag| 1 | 0x00 = None, 0x01 = Some |
|
||||
/// | receiving_chain | 0 or 32 | Chain key if flag is 0x01 |
|
||||
/// | dh_self_secret | 32 | DH secret key |
|
||||
/// | dh_remote_flag | 1 | 0x00 = None, 0x01 = Some |
|
||||
/// | dh_remote | 0 or 32 | DH public key if flag is 0x01 |
|
||||
/// | msg_send | 4 | Send counter (big-endian) |
|
||||
/// | msg_recv | 4 | Receive counter (big-endian) |
|
||||
/// | prev_chain_len | 4 | Previous chain length (big-endian) |
|
||||
/// | skipped_count | 4 | Number of skipped keys (big-endian) |
|
||||
/// | skipped_keys | 68 * count | Each: pubkey(32) + msg_num(4) + key(32) |
|
||||
/// ```
|
||||
pub fn as_bytes(&self) -> Zeroizing<Vec<u8>> {
|
||||
fn option_size(opt: Option<[u8; 32]>) -> usize {
|
||||
1 + opt.map_or(0, |_| 32)
|
||||
}
|
||||
|
||||
fn write_option(buf: &mut Vec<u8>, opt: Option<[u8; 32]>) {
|
||||
match opt {
|
||||
Some(data) => {
|
||||
buf.push(0x01);
|
||||
buf.extend_from_slice(&data);
|
||||
}
|
||||
None => buf.push(0x00),
|
||||
}
|
||||
}
|
||||
|
||||
let skipped_count = self.skipped_keys.len();
|
||||
let dh_remote = self.dh_remote.map(|pk| pk.to_bytes());
|
||||
|
||||
let capacity = 1 + 32 // version + root_key
|
||||
+ option_size(self.sending_chain)
|
||||
+ option_size(self.receiving_chain)
|
||||
+ 32 // dh_self
|
||||
+ option_size(dh_remote)
|
||||
+ 12 // counters
|
||||
+ 4 + (skipped_count * 68); // skipped keys
|
||||
|
||||
let mut buf = Zeroizing::new(Vec::with_capacity(capacity));
|
||||
|
||||
buf.push(SERIALIZATION_VERSION);
|
||||
buf.extend_from_slice(&self.root_key);
|
||||
write_option(&mut buf, self.sending_chain);
|
||||
write_option(&mut buf, self.receiving_chain);
|
||||
|
||||
let dh_secret = self.dh_self.secret_bytes();
|
||||
buf.extend_from_slice(dh_secret);
|
||||
|
||||
write_option(&mut buf, dh_remote);
|
||||
|
||||
buf.extend_from_slice(&self.msg_send.to_be_bytes());
|
||||
buf.extend_from_slice(&self.msg_recv.to_be_bytes());
|
||||
buf.extend_from_slice(&self.prev_chain_len.to_be_bytes());
|
||||
|
||||
buf.extend_from_slice(&(skipped_count as u32).to_be_bytes());
|
||||
for ((pk, msg_num), mk) in &self.skipped_keys {
|
||||
buf.extend_from_slice(pk.as_bytes());
|
||||
buf.extend_from_slice(&msg_num.to_be_bytes());
|
||||
buf.extend_from_slice(mk);
|
||||
}
|
||||
|
||||
buf
|
||||
}
|
||||
|
||||
/// Deserializes a ratchet state from binary data.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `RatchetError::DeserializationFailed` if the data is invalid or truncated.
|
||||
pub fn from_bytes(data: &[u8]) -> Result<Self, RatchetError> {
|
||||
let mut reader = Reader::new(data);
|
||||
|
||||
let version = reader.read_u8()?;
|
||||
if version != SERIALIZATION_VERSION {
|
||||
return Err(RatchetError::DeserializationFailed);
|
||||
}
|
||||
|
||||
let root_key: RootKey = reader.read_array()?;
|
||||
let sending_chain = reader.read_option()?;
|
||||
let receiving_chain = reader.read_option()?;
|
||||
|
||||
let mut dh_self_bytes: [u8; 32] = reader.read_array()?;
|
||||
let dh_self = InstallationKeyPair::from_secret_bytes(dh_self_bytes);
|
||||
dh_self_bytes.zeroize();
|
||||
|
||||
let dh_remote = reader.read_option()?.map(PublicKey::from);
|
||||
|
||||
let msg_send = reader.read_u32()?;
|
||||
let msg_recv = reader.read_u32()?;
|
||||
let prev_chain_len = reader.read_u32()?;
|
||||
|
||||
let skipped_count = reader.read_u32()? as usize;
|
||||
let mut skipped_keys = HashMap::with_capacity(skipped_count);
|
||||
for _ in 0..skipped_count {
|
||||
let pk = PublicKey::from(reader.read_array::<32>()?);
|
||||
let msg_num = reader.read_u32()?;
|
||||
let mk: MessageKey = reader.read_array()?;
|
||||
skipped_keys.insert((pk, msg_num), mk);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
root_key,
|
||||
sending_chain,
|
||||
receiving_chain,
|
||||
dh_self,
|
||||
dh_remote,
|
||||
msg_send,
|
||||
msg_recv,
|
||||
prev_chain_len,
|
||||
skipped_keys,
|
||||
_domain: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom serde Serialize implementation that uses our binary format.
|
||||
impl<D: HkdfInfo> Serialize for RatchetState<D> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_bytes(&self.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom serde Deserialize implementation that uses our binary format.
|
||||
impl<'de, D: HkdfInfo> Deserialize<'de> for RatchetState<D> {
|
||||
fn deserialize<De>(deserializer: De) -> Result<Self, De::Error>
|
||||
where
|
||||
De: Deserializer<'de>,
|
||||
{
|
||||
let bytes = <Vec<u8>>::deserialize(deserializer)?;
|
||||
Self::from_bytes(&bytes).map_err(DeError::custom)
|
||||
}
|
||||
}
|
||||
|
||||
/// Public header attached to every encrypted message (unencrypted but authenticated).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Header {
|
||||
@ -513,6 +666,156 @@ mod tests {
|
||||
assert_eq!(result.unwrap_err(), RatchetError::MessageReplay);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_deserialize_sender_state() {
|
||||
let (alice, _, _) = setup_alice_bob();
|
||||
|
||||
// Serialize to binary
|
||||
let bytes = alice.as_bytes();
|
||||
|
||||
// Deserialize back
|
||||
let restored: RatchetState = RatchetState::from_bytes(&bytes).unwrap();
|
||||
|
||||
// Verify key fields match
|
||||
assert_eq!(alice.root_key, restored.root_key);
|
||||
assert_eq!(alice.sending_chain, restored.sending_chain);
|
||||
assert_eq!(alice.receiving_chain, restored.receiving_chain);
|
||||
assert_eq!(alice.msg_send, restored.msg_send);
|
||||
assert_eq!(alice.msg_recv, restored.msg_recv);
|
||||
assert_eq!(alice.prev_chain_len, restored.prev_chain_len);
|
||||
assert_eq!(
|
||||
alice.dh_remote.map(|pk| pk.to_bytes()),
|
||||
restored.dh_remote.map(|pk| pk.to_bytes())
|
||||
);
|
||||
assert_eq!(
|
||||
alice.dh_self.public().to_bytes(),
|
||||
restored.dh_self.public().to_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_deserialize_receiver_state() {
|
||||
let (_, bob, _) = setup_alice_bob();
|
||||
|
||||
// Serialize to binary
|
||||
let bytes = bob.as_bytes();
|
||||
|
||||
// Deserialize back
|
||||
let restored: RatchetState = RatchetState::from_bytes(&bytes).unwrap();
|
||||
|
||||
// Verify key fields match
|
||||
assert_eq!(bob.root_key, restored.root_key);
|
||||
assert_eq!(bob.sending_chain, restored.sending_chain);
|
||||
assert_eq!(bob.receiving_chain, restored.receiving_chain);
|
||||
assert_eq!(bob.msg_send, restored.msg_send);
|
||||
assert_eq!(bob.msg_recv, restored.msg_recv);
|
||||
assert_eq!(bob.prev_chain_len, restored.prev_chain_len);
|
||||
assert!(bob.dh_remote.is_none());
|
||||
assert!(restored.dh_remote.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_deserialize_with_skipped_keys() {
|
||||
let (mut alice, mut bob, _) = setup_alice_bob();
|
||||
|
||||
// Alice sends 3 messages
|
||||
let mut sent = vec![];
|
||||
for i in 0..3 {
|
||||
let plaintext = format!("Message {}", i + 1).into_bytes();
|
||||
let (ct, header) = alice.encrypt_message(&plaintext);
|
||||
sent.push((ct, header, plaintext));
|
||||
}
|
||||
|
||||
// Bob receives only msg0 and msg2, skipping msg1
|
||||
bob.decrypt_message(&sent[0].0, sent[0].1.clone()).unwrap();
|
||||
bob.decrypt_message(&sent[2].0, sent[2].1.clone()).unwrap();
|
||||
|
||||
// Bob should have one skipped key
|
||||
assert_eq!(bob.skipped_keys.len(), 1);
|
||||
|
||||
// Serialize Bob's state
|
||||
let bytes = bob.as_bytes();
|
||||
|
||||
// Deserialize
|
||||
let mut restored: RatchetState = RatchetState::from_bytes(&bytes).unwrap();
|
||||
|
||||
// Restored state should have the skipped key
|
||||
assert_eq!(restored.skipped_keys.len(), 1);
|
||||
|
||||
// The restored state should be able to decrypt the skipped message
|
||||
let pt1 = restored
|
||||
.decrypt_message(&sent[1].0, sent[1].1.clone())
|
||||
.unwrap();
|
||||
assert_eq!(pt1, sent[1].2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_deserialize_continue_conversation() {
|
||||
let (mut alice, mut bob, _) = setup_alice_bob();
|
||||
|
||||
// Exchange some messages
|
||||
let (ct1, h1) = alice.encrypt_message(b"Hello Bob");
|
||||
bob.decrypt_message(&ct1, h1).unwrap();
|
||||
|
||||
let (ct2, h2) = bob.encrypt_message(b"Hello Alice");
|
||||
alice.decrypt_message(&ct2, h2).unwrap();
|
||||
|
||||
// Serialize both states
|
||||
let alice_bytes = alice.as_bytes();
|
||||
let bob_bytes = bob.as_bytes();
|
||||
|
||||
// Deserialize
|
||||
let mut alice_restored: RatchetState = RatchetState::from_bytes(&alice_bytes).unwrap();
|
||||
let mut bob_restored: RatchetState = RatchetState::from_bytes(&bob_bytes).unwrap();
|
||||
|
||||
// Continue the conversation with restored states
|
||||
let (ct3, h3) = alice_restored.encrypt_message(b"Message after restore");
|
||||
let pt3 = bob_restored.decrypt_message(&ct3, h3).unwrap();
|
||||
assert_eq!(pt3, b"Message after restore");
|
||||
|
||||
let (ct4, h4) = bob_restored.encrypt_message(b"Reply after restore");
|
||||
let pt4 = alice_restored.decrypt_message(&ct4, h4).unwrap();
|
||||
assert_eq!(pt4, b"Reply after restore");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialization_version_check() {
|
||||
let (alice, _, _) = setup_alice_bob();
|
||||
let mut bytes = alice.as_bytes();
|
||||
|
||||
// Tamper with version byte
|
||||
bytes[0] = 0xFF;
|
||||
|
||||
let result = RatchetState::<DefaultDomain>::from_bytes(&bytes);
|
||||
assert!(matches!(result, Err(RatchetError::DeserializationFailed)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialization_truncated_data() {
|
||||
let (alice, _, _) = setup_alice_bob();
|
||||
let bytes = alice.as_bytes();
|
||||
|
||||
// Truncate the data
|
||||
let truncated = &bytes[..10];
|
||||
|
||||
let result = RatchetState::<DefaultDomain>::from_bytes(truncated);
|
||||
assert!(matches!(result, Err(RatchetError::DeserializationFailed)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialization_size_efficiency() {
|
||||
let (alice, _, _) = setup_alice_bob();
|
||||
let bytes = alice.as_bytes();
|
||||
|
||||
// Minimum size: version(1) + root_key(32) + sending_flag(1) + sending(32) +
|
||||
// receiving_flag(1) + dh_self(32) + dh_remote_flag(1) + dh_remote(32) +
|
||||
// counters(12) + skipped_count(4) = 148 bytes for sender with no skipped keys
|
||||
assert!(bytes.len() < 200, "Serialized size should be compact");
|
||||
|
||||
// Verify version byte
|
||||
assert_eq!(bytes[0], 1, "Version should be 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_skipped_keys_export() {
|
||||
let (mut alice, mut bob, _) = setup_alice_bob();
|
||||
|
||||
@ -26,7 +26,7 @@ impl<D: HkdfInfo> From<&RatchetState<D>> for RatchetStateRecord {
|
||||
root_key: state.root_key,
|
||||
sending_chain: state.sending_chain,
|
||||
receiving_chain: state.receiving_chain,
|
||||
dh_self_secret: state.dh_self.secret_bytes(),
|
||||
dh_self_secret: *state.dh_self.secret_bytes(),
|
||||
dh_remote: state.dh_remote.map(|pk| pk.to_bytes()),
|
||||
msg_send: state.msg_send,
|
||||
msg_recv: state.msg_recv,
|
||||
|
||||
8
nim-bindings/README.md
Normal file
8
nim-bindings/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Nim-bindings
|
||||
|
||||
A Nim wrapping class that exposes LibChat functionality.
|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
`nimble pingpong` - Run the pingpong example.
|
||||
21
nim-bindings/conversations_example.nimble
Normal file
21
nim-bindings/conversations_example.nimble
Normal file
@ -0,0 +1,21 @@
|
||||
# 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"
|
||||
|
||||
# Build Rust library before compiling Nim
|
||||
before build:
|
||||
exec "cargo build --release --manifest-path ../Cargo.toml"
|
||||
|
||||
task pingpong, "Run pingpong example":
|
||||
exec "nim c -r --path:src examples/pingpong.nim"
|
||||
35
nim-bindings/examples/pingpong.nim
Normal file
35
nim-bindings/examples/pingpong.nim
Normal file
@ -0,0 +1,35 @@
|
||||
import options
|
||||
import results
|
||||
|
||||
import ../src/libchat
|
||||
|
||||
proc pingpong() =
|
||||
|
||||
var raya = newConversationsContext()
|
||||
var saro = newConversationsContext()
|
||||
|
||||
|
||||
# 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, "Hey Raya").expect("[Saro] Couldn't create convo")
|
||||
echo "ConvoHandle:: ", 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()
|
||||
|
||||
175
nim-bindings/src/bindings.nim
Normal file
175
nim-bindings/src/bindings.nim
Normal file
@ -0,0 +1,175 @@
|
||||
# Nim FFI bindings for libchat conversations library
|
||||
|
||||
import std/[os]
|
||||
|
||||
# Dynamic library path resolution
|
||||
# Can be overridden at compile time with -d:CONVERSATIONS_LIB:"path/to/lib"
|
||||
# Or at runtime via LIBCHAT_LIB environment variable
|
||||
when defined(macosx):
|
||||
const DEFAULT_LIB_NAME = "liblogos_chat.dylib"
|
||||
elif defined(linux):
|
||||
const DEFAULT_LIB_NAME = "liblogos_chat.so"
|
||||
elif defined(windows):
|
||||
const DEFAULT_LIB_NAME = "logos_chat.dll"
|
||||
else:
|
||||
const DEFAULT_LIB_NAME = "logos_chat"
|
||||
|
||||
# Try to find the library relative to the source file location at compile time
|
||||
const
|
||||
thisDir = currentSourcePath().parentDir()
|
||||
projectRoot = thisDir.parentDir().parentDir()
|
||||
releaseLibPath = projectRoot / "target" / "release" / DEFAULT_LIB_NAME
|
||||
debugLibPath = projectRoot / "target" / "debug" / DEFAULT_LIB_NAME
|
||||
|
||||
# Default to release path, can be overridden with -d:CONVERSATIONS_LIB:"..."
|
||||
const CONVERSATIONS_LIB* {.strdefine.} = releaseLibPath
|
||||
|
||||
# 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 ConvoHandle* = uint32
|
||||
|
||||
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
|
||||
|
||||
## Result structure for create_intro_bundle
|
||||
## error_code is 0 on success, negative on error (see ErrorCode)
|
||||
PayloadResult* = object
|
||||
error_code*: int32
|
||||
payloads*: VecPayload
|
||||
|
||||
## Result from create_new_private_convo
|
||||
## error_code is 0 on success, negative on error (see ErrorCode)
|
||||
NewConvoResult* = object
|
||||
error_code*: int32
|
||||
convo_id*: uint32
|
||||
payloads*: VecPayload
|
||||
|
||||
# FFI function imports
|
||||
|
||||
## Creates a new libchat Context
|
||||
## Returns: Opaque handle to the context. Must be freed with destroy_context()
|
||||
proc create_context*(): ContextHandle {.importc, dynlib: CONVERSATIONS_LIB.}
|
||||
|
||||
## 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, dynlib: CONVERSATIONS_LIB.}
|
||||
|
||||
## Creates an intro bundle for sharing with other users
|
||||
## Returns: Number of bytes written to bundle_out, or negative error code
|
||||
proc create_intro_bundle*(
|
||||
ctx: ContextHandle,
|
||||
bundle_out: SliceUint8,
|
||||
): int32 {.importc, dynlib: CONVERSATIONS_LIB.}
|
||||
|
||||
## 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, dynlib: CONVERSATIONS_LIB.}
|
||||
|
||||
## Sends content to an existing conversation
|
||||
## Returns: PayloadResult struct - check error_code field (0 = success, negative = error)
|
||||
## The result must be freed with destroy_payload_result()
|
||||
proc send_content*(
|
||||
ctx: ContextHandle,
|
||||
convo_handle: ConvoHandle,
|
||||
content: SliceUint8,
|
||||
): PayloadResult {.importc, dynlib: CONVERSATIONS_LIB.}
|
||||
|
||||
## Handles an incoming payload and writes content to caller-provided buffers
|
||||
## Returns: Number of bytes written to content_out on success (>= 0), negative error code on failure
|
||||
## conversation_id_out_len is set to the number of bytes written to conversation_id_out
|
||||
proc handle_payload*(
|
||||
ctx: ContextHandle,
|
||||
payload: SliceUint8,
|
||||
conversation_id_out: SliceUint8,
|
||||
conversation_id_out_len: ptr uint32,
|
||||
content_out: SliceUint8,
|
||||
): int32 {.importc, dynlib: CONVERSATIONS_LIB.}
|
||||
|
||||
## Free the result from create_new_private_convo
|
||||
proc destroy_convo_result*(result: NewConvoResult) {.importc, dynlib: CONVERSATIONS_LIB.}
|
||||
|
||||
## Free the PayloadResult
|
||||
proc destroy_payload_result*(result: PayloadResult) {.importc, dynlib: CONVERSATIONS_LIB.}
|
||||
|
||||
# ============================================================================
|
||||
# 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)
|
||||
|
||||
## 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)
|
||||
159
nim-bindings/src/libchat.nim
Normal file
159
nim-bindings/src/libchat.nim
Normal file
@ -0,0 +1,159 @@
|
||||
import std/options
|
||||
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*(): LibChat =
|
||||
|
||||
result.handle = create_context()
|
||||
result.buffer_size = 256
|
||||
if result.handle.isNil:
|
||||
raise newException(IOError, "Failed to create context")
|
||||
|
||||
## 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[string, string] =
|
||||
if ctx.handle == nil:
|
||||
return err("Context handle is nil")
|
||||
|
||||
var buffer = ctx.getBuffer()
|
||||
var slice = buffer.toSlice()
|
||||
let len = create_intro_bundle(ctx.handle, slice)
|
||||
|
||||
if len < 0:
|
||||
return err("Failed to create intro bundle: " & $len)
|
||||
|
||||
buffer.setLen(len)
|
||||
return ok(cast[string](buffer))
|
||||
|
||||
## Create a Private Convo
|
||||
proc createNewPrivateConvo*(ctx: LibChat, bundle: string, content: string): Result[(ConvoHandle, 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()
|
||||
)
|
||||
|
||||
if res.error_code != 0:
|
||||
result = err("Failed to create private convo: " & $res.error_code)
|
||||
destroy_convo_result(res)
|
||||
return
|
||||
|
||||
# Convert payloads to Nim types
|
||||
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()
|
||||
)
|
||||
|
||||
let convoId = res.convo_id
|
||||
|
||||
# Free the result
|
||||
destroy_convo_result(res)
|
||||
|
||||
return ok((convoId, payloads))
|
||||
|
||||
## Send content to an existing conversation
|
||||
proc sendContent*(ctx: LibChat, convoHandle: ConvoHandle, content: string): 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,
|
||||
convoHandle,
|
||||
content.toSlice()
|
||||
)
|
||||
|
||||
if res.error_code != 0:
|
||||
result = err("Failed to send content: " & $res.error_code)
|
||||
destroy_payload_result(res)
|
||||
return
|
||||
|
||||
# Convert payloads to Nim types
|
||||
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()
|
||||
)
|
||||
|
||||
destroy_payload_result(res)
|
||||
return ok(payloads)
|
||||
|
||||
type
|
||||
ContentResult* = object
|
||||
conversationId*: string
|
||||
data*: seq[uint8]
|
||||
|
||||
## 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")
|
||||
|
||||
var conversationIdBuf = newSeq[byte](ctx.buffer_size)
|
||||
var contentBuf = newSeq[byte](ctx.buffer_size)
|
||||
var conversationIdLen: uint32 = 0
|
||||
|
||||
let bytesWritten = bindings.handle_payload(
|
||||
ctx.handle,
|
||||
payload.toSlice(),
|
||||
conversationIdBuf.toSlice(),
|
||||
addr conversationIdLen,
|
||||
contentBuf.toSlice()
|
||||
)
|
||||
|
||||
if bytesWritten < 0:
|
||||
return err("Failed to handle payload: " & $bytesWritten)
|
||||
|
||||
if bytesWritten == 0:
|
||||
return ok(none(ContentResult))
|
||||
|
||||
conversationIdBuf.setLen(conversationIdLen)
|
||||
contentBuf.setLen(bytesWritten)
|
||||
|
||||
return ok(some(ContentResult(
|
||||
conversationId: cast[string](conversationIdBuf),
|
||||
data: contentBuf
|
||||
)))
|
||||
|
||||
|
||||
proc `=destroy`(x: var LibChat) =
|
||||
# Automatically free handle when the destructor is called
|
||||
if x.handle != nil:
|
||||
x.destroy()
|
||||
Loading…
x
Reference in New Issue
Block a user