diff --git a/core/conversations/src/api.rs b/core/conversations/src/api.rs index 3041b1f..a189627 100644 --- a/core/conversations/src/api.rs +++ b/core/conversations/src/api.rs @@ -47,11 +47,13 @@ pub struct ContextHandle(pub(crate) Context); /// Creates a new libchat Ctx /// /// # Returns -/// Opaque handle to the store. Must be freed with destroy_context() +/// Opaque handle to the store. Must be freed with destroy_context(). +/// Uses lossy UTF-8 conversion: invalid bytes are replaced with U+FFFD +/// so the caller always gets a deterministic name reflecting their input. #[ffi_export] pub fn create_context(name: c_slice::Ref<'_, u8>) -> repr_c::Box { - let name_str = std::str::from_utf8(name.as_slice()).unwrap_or("default"); - Box::new(ContextHandle(Context::new_with_name(name_str))).into() + let name_str = std::string::String::from_utf8_lossy(name.as_slice()); + Box::new(ContextHandle(Context::new_with_name(&name_str))).into() } /// Returns the friendly name of the contexts installation. @@ -193,7 +195,16 @@ pub fn send_content( content: c_slice::Ref<'_, u8>, out: &mut SendContentResult, ) { - let convo_id_str = std::str::from_utf8(convo_id.as_slice()).unwrap_or(""); + let convo_id_str = match std::str::from_utf8(convo_id.as_slice()) { + Ok(s) => s, + Err(_) => { + *out = SendContentResult { + error_code: ErrorCode::BadConvoId as i32, + payloads: safer_ffi::Vec::EMPTY, + }; + return; + } + }; let payloads = match ctx.0.send_content(convo_id_str, &content) { Ok(p) => p, Err(_) => { diff --git a/core/conversations/src/conversation/privatev1.rs b/core/conversations/src/conversation/privatev1.rs index 0289be5..080f315 100644 --- a/core/conversations/src/conversation/privatev1.rs +++ b/core/conversations/src/conversation/privatev1.rs @@ -60,7 +60,11 @@ pub struct PrivateV1Convo { } impl PrivateV1Convo { - pub fn new_initiator(seed_key: SymmetricKey32, remote: PublicKey, remote_delivery_address: String) -> Self { + pub fn new_initiator( + seed_key: SymmetricKey32, + remote: PublicKey, + remote_delivery_address: String, + ) -> Self { let base_convo_id = BaseConvoId::new(&seed_key); let local_convo_id = base_convo_id.id_for_participant(Role::Initiator); let remote_convo_id = base_convo_id.id_for_participant(Role::Responder); @@ -79,7 +83,11 @@ impl PrivateV1Convo { } } - pub fn new_responder(seed_key: SymmetricKey32, dh_self: &PrivateKey, remote_delivery_address: String) -> Self { + pub fn new_responder( + seed_key: SymmetricKey32, + dh_self: &PrivateKey, + remote_delivery_address: String, + ) -> Self { let base_convo_id = BaseConvoId::new(&seed_key); let local_convo_id = base_convo_id.id_for_participant(Role::Responder); let remote_convo_id = base_convo_id.id_for_participant(Role::Initiator); @@ -239,7 +247,8 @@ mod tests { let seed_key_saro = SymmetricKey32::from(seed_key); let seed_key_raya = SymmetricKey32::from(seed_key); let send_content_bytes = vec![0, 2, 4, 6, 8]; - let mut sr_convo = PrivateV1Convo::new_initiator(seed_key_saro, pub_raya, "test_addr".into()); + let mut sr_convo = + PrivateV1Convo::new_initiator(seed_key_saro, pub_raya, "test_addr".into()); let mut rs_convo = PrivateV1Convo::new_responder(seed_key_raya, &raya, "test_addr".into()); let send_frame = PrivateV1Frame { diff --git a/core/conversations/src/inbox/handler.rs b/core/conversations/src/inbox/handler.rs index 6752b3a..6b1862e 100644 --- a/core/conversations/src/inbox/handler.rs +++ b/core/conversations/src/inbox/handler.rs @@ -72,8 +72,13 @@ impl Inbox { let (seed_key, ephemeral_pub) = InboxHandshake::perform_as_initiator(self.ident.secret(), &pkb, &mut rng); - let remote_delivery_addr = Inbox::inbox_identifier_for_key(*remote_bundle.installation_key()); - let mut convo = PrivateV1Convo::new_initiator(seed_key, *remote_bundle.ephemeral_key(), remote_delivery_addr.clone()); + let remote_delivery_addr = + Inbox::inbox_identifier_for_key(*remote_bundle.installation_key()); + let mut convo = PrivateV1Convo::new_initiator( + seed_key, + *remote_bundle.ephemeral_key(), + remote_delivery_addr.clone(), + ); let mut payloads = convo.send_message(initial_message)?; @@ -119,17 +124,21 @@ impl Inbox { let ephemeral_key = self.lookup_ephemeral_key(&key_index)?; // Extract initiator's identity key for delivery address before header is consumed - let initiator_static_bytes: [u8; 32] = header.initiator_static.as_ref() + let initiator_static_bytes: [u8; 32] = header + .initiator_static + .as_ref() .try_into() .map_err(|_| ChatError::BadBundleValue("wrong size - initiator static".into()))?; - let remote_delivery_addr = Inbox::inbox_identifier_for_key(PublicKey::from(initiator_static_bytes)); + let remote_delivery_addr = + Inbox::inbox_identifier_for_key(PublicKey::from(initiator_static_bytes)); // Perform handshake and decrypt frame let (seed_key, frame) = self.perform_handshake(ephemeral_key, header, handshake.payload)?; match frame.frame_type.unwrap() { proto::inbox_v1_frame::FrameType::InvitePrivateV1(_invite_private_v1) => { - let mut convo = PrivateV1Convo::new_responder(seed_key, ephemeral_key, remote_delivery_addr); + let mut convo = + PrivateV1Convo::new_responder(seed_key, ephemeral_key, remote_delivery_addr); let Some(enc_payload) = _invite_private_v1.initial_message else { return Err(ChatError::Protocol("missing initial encpayload".into())); diff --git a/flake.nix b/flake.nix index 7653b8c..ea8d250 100644 --- a/flake.nix +++ b/flake.nix @@ -51,18 +51,24 @@ doCheck = false; # tests require network access unavailable in nix sandbox postBuild = '' - cargo run --release --bin generate-libchat-headers --features headers + cargo run --frozen --release --bin generate-libchat-headers --features headers ''; installPhase = '' runHook preInstall mkdir -p $out/lib $out/include - # Copy shared library + # Copy shared library (platform-dependent extension) cp target/release/liblibchat.so $out/lib/ 2>/dev/null || true cp target/release/liblibchat.dylib $out/lib/ 2>/dev/null || true cp target/release/liblibchat.a $out/lib/ 2>/dev/null || true + # Fail if no library was produced + if [ -z "$(ls $out/lib/liblibchat.* 2>/dev/null)" ]; then + echo "ERROR: No library artifact found in target/release/" + exit 1 + fi + # Copy generated header cp libchat.h $out/include/ diff --git a/nim-bindings/src/bindings.nim b/nim-bindings/src/bindings.nim index eb6053e..aa7310a 100644 --- a/nim-bindings/src/bindings.nim +++ b/nim-bindings/src/bindings.nim @@ -85,7 +85,7 @@ type ## Creates a new libchat Context ## Returns: Opaque handle to the context. Must be freed with destroy_context() -proc create_context*(name: ReprCString): ContextHandle {.importc.} +proc create_context*(name: SliceUint8): ContextHandle {.importc.} ## Returns the friendly name of the context's identity ## The result must be freed by the caller (repr_c::String ownership transfers) @@ -129,7 +129,7 @@ proc list_conversations*( ## The result must be freed with destroy_send_content_result() proc send_content*( ctx: ContextHandle, - convo_id: ReprCString, + convo_id: SliceUint8, content: SliceUint8, ): SendContentResult {.importc.} diff --git a/nim-bindings/tests/test_all_endpoints.nim b/nim-bindings/tests/test_all_endpoints.nim index e9e3f54..bc12f95 100644 --- a/nim-bindings/tests/test_all_endpoints.nim +++ b/nim-bindings/tests/test_all_endpoints.nim @@ -72,7 +72,7 @@ proc testHelperProcs() = proc testContextLifecycle() = echo "\n--- testContextLifecycle ---" - let ctx = create_context(toReprCString("lifecycle-test")) + let ctx = create_context(toSlice("lifecycle-test")) check(ctx != nil, "create_context: returns non-nil handle") let iname = installation_name(ctx) @@ -94,10 +94,10 @@ proc testContextLifecycle() = proc testFullConversationFlow() = echo "\n--- testFullConversationFlow ---" - let aliceCtx = create_context(toReprCString("alice")) + let aliceCtx = create_context(toSlice("alice")) check(aliceCtx != nil, "Alice: create_context non-nil") - let bobCtx = create_context(toReprCString("bob")) + let bobCtx = create_context(toSlice("bob")) check(bobCtx != nil, "Bob: create_context non-nil") # --- create_intro_bundle --- @@ -177,7 +177,7 @@ proc testFullConversationFlow() = # --- send_content --- var sendRes = send_content( aliceCtx, - toReprCString(aliceConvoId), + toSlice(aliceConvoId), toSlice("How are you, Bob?") ) check(sendRes.error_code == ErrNone, @@ -213,13 +213,13 @@ proc testFullConversationFlow() = proc testErrorCases() = echo "\n--- testErrorCases ---" - let ctx = create_context(toReprCString("error-tester")) + let ctx = create_context(toSlice("error-tester")) check(ctx != nil, "error-tester: create_context non-nil") # send_content with a nonexistent convo_id must fail var badSend = send_content( ctx, - toReprCString("00000000-0000-0000-0000-nonexistent"), + toSlice("00000000-0000-0000-0000-nonexistent"), toSlice("payload") ) check(badSend.error_code != ErrNone,