Skip to main content

simploxide_sxcrt_sys/
lib.rs

1use serde::Deserialize;
2
3use std::{
4    ffi::{CStr, CString, NulError, c_char, c_int, c_void},
5    sync::Once,
6};
7
8/// TODO: expose more methods on demand
9#[allow(unused)]
10#[allow(non_camel_case_types)]
11mod bindings;
12
13static HASKELL_RUNTIME: Once = Once::new();
14
15type Handle = bindings::chat_ctrl;
16
17pub struct SimpleXChat(Handle);
18
19impl SimpleXChat {
20    pub fn init(
21        db_path: String,
22        db_key: String,
23        migration: MigrationConfirmation,
24    ) -> Result<Self, InitError> {
25        HASKELL_RUNTIME.call_once(haskell_init);
26
27        let mut handle: Handle = std::ptr::null_mut();
28        let db_path = CString::new(db_path).map_err(CallError::NullByteInput)?;
29        let db_key = CString::new(db_key).map_err(CallError::NullByteInput)?;
30        let string = Self::init_raw(&db_path, &db_key, migration.as_cstr(), &mut handle)?;
31
32        #[derive(Deserialize)]
33        struct Response<'a> {
34            #[serde(borrow, rename = "type")]
35            type_: &'a str,
36        }
37
38        let response: Response<'_> =
39            serde_json::from_str(&string).map_err(CallError::InvalidJson)?;
40
41        if response.type_ == "ok" {
42            Ok(Self(handle))
43        } else {
44            let error = serde_json::from_str(&string).map_err(CallError::InvalidJson)?;
45            Err(InitError::DbError(error))
46        }
47    }
48
49    pub fn send_cmd(&mut self, cmd: String) -> Result<String, CallError> {
50        let ccmd = CString::new(cmd)?;
51        let mut c_res = unsafe { bindings::chat_send_cmd(self.0, ccmd.as_ptr()) };
52        drop(ccmd);
53        c_res_to_string(&mut c_res)
54    }
55
56    /// [`recv_msg_wait`](Self::recv_msg_wait) but with minimum possible wait duration
57    pub fn try_recv_msg(&mut self) -> Result<String, CallError> {
58        self.recv_msg_wait(std::time::Duration::from_micros(1))
59    }
60
61    pub fn recv_msg_wait(&mut self, wait: std::time::Duration) -> Result<String, CallError> {
62        let clamped = std::cmp::min(wait, std::time::Duration::from_mins(30));
63
64        // SAFETY: clamped to fit into i32 without overflows
65        let cwait: c_int = clamped.as_micros() as i32;
66        let mut c_res = unsafe { bindings::chat_recv_msg_wait(self.0, cwait) };
67
68        c_res_to_string(&mut c_res)
69    }
70
71    fn init_raw(
72        db_path: &CStr,
73        db_key: &CStr,
74        migration: &'static CStr,
75        handle: &mut Handle,
76    ) -> Result<String, CallError> {
77        let mut c_res = unsafe {
78            bindings::chat_migrate_init(
79                db_path.as_ptr(),
80                db_key.as_ptr(),
81                migration.as_ptr(),
82                handle,
83            )
84        };
85
86        c_res_to_string(&mut c_res)
87    }
88}
89
90impl Drop for SimpleXChat {
91    fn drop(&mut self) {
92        unsafe {
93            bindings::chat_close_store(self.0);
94        }
95    }
96}
97
98#[derive(Debug, Clone, Copy)]
99pub enum MigrationConfirmation {
100    YesUp,
101    YesUpDown,
102    Console,
103    Error,
104}
105
106impl MigrationConfirmation {
107    fn as_cstr(&self) -> &'static CStr {
108        match self {
109            Self::YesUp => c"yesUp",
110            Self::YesUpDown => c"yesUpDown",
111            Self::Console => c"console",
112            Self::Error => c"error",
113        }
114    }
115}
116
117fn haskell_init() {
118    #[cfg(target_os = "windows")]
119    let args = Box::new([
120        c"simplex".as_ptr() as *mut c_char,
121        c"+RTS".as_ptr() as *mut c_char,
122        c"-A64m".as_ptr() as *mut c_char,
123        c"-H64m".as_ptr() as *mut c_char,
124        c"--install-signal-handlers=no".as_ptr() as *mut c_char,
125        std::ptr::null_mut(),
126    ]);
127
128    #[cfg(not(target_os = "windows"))]
129    let args = Box::new([
130        c"simplex".as_ptr() as *mut c_char,
131        c"+RTS".as_ptr() as *mut c_char,
132        c"-A64m".as_ptr() as *mut c_char,
133        c"-H64m".as_ptr() as *mut c_char,
134        c"-xn".as_ptr() as *mut c_char,
135        c"--install-signal-handlers=no".as_ptr() as *mut c_char,
136        std::ptr::null_mut(),
137    ]);
138
139    let mut argc: c_int = (args.len() - 1) as c_int;
140    let mut pargv: *mut *mut c_char = Box::leak(args).as_mut_ptr();
141
142    unsafe {
143        bindings::hs_init_with_rtsopts(&mut argc, &mut pargv);
144    }
145}
146
147fn c_res_to_string(c_res: &mut *mut c_char) -> Result<String, CallError> {
148    fn try_parse_c_res(c_res: *mut c_char) -> Result<String, CallError> {
149        if c_res.is_null() {
150            return Err(CallError::Failure);
151        }
152
153        // SAFETY:
154        // * SimpleX-Core-FFI functions should return valid null-terminated C strings
155        // * c_res ptr is not null(checked above)
156        // * c_res memory is not mutating and is hold exclusively while CStr::from_ptr borrow is
157        //   active(ensured by &mut in the outer method)
158        let string = unsafe { CStr::from_ptr(c_res).to_str()?.to_owned() };
159        Ok(string)
160    }
161
162    let parsed = try_parse_c_res(*c_res);
163
164    unsafe {
165        libc::free(*c_res as *mut c_void);
166    }
167    *c_res = std::ptr::null_mut();
168
169    parsed
170}
171
172#[derive(Debug)]
173pub enum InitError {
174    CallError(CallError),
175    DbError(serde_json::Value),
176}
177
178impl From<CallError> for InitError {
179    fn from(value: CallError) -> Self {
180        Self::CallError(value)
181    }
182}
183
184impl std::fmt::Display for InitError {
185    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186        match self {
187            InitError::CallError(call_error) => call_error.fmt(f),
188            InitError::DbError(value) => {
189                write!(f, "cannot create DB connection:\n{value:#}")
190            }
191        }
192    }
193}
194
195impl std::error::Error for InitError {
196    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
197        match self {
198            Self::CallError(call_error) => Some(call_error),
199            Self::DbError(_) => None,
200        }
201    }
202}
203
204#[derive(Debug)]
205pub enum CallError {
206    NullByteInput(NulError),
207    Failure,
208    NotUtf8(std::str::Utf8Error),
209    InvalidJson(serde_json::Error),
210}
211
212impl From<NulError> for CallError {
213    fn from(value: NulError) -> Self {
214        Self::NullByteInput(value)
215    }
216}
217
218impl From<std::str::Utf8Error> for CallError {
219    fn from(value: std::str::Utf8Error) -> Self {
220        Self::NotUtf8(value)
221    }
222}
223
224impl From<serde_json::Error> for CallError {
225    fn from(value: serde_json::Error) -> Self {
226        Self::InvalidJson(value)
227    }
228}
229
230impl std::fmt::Display for CallError {
231    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232        match self {
233            CallError::NullByteInput(error) => {
234                write!(f, "null byte injection in one of the input strings {error}")
235            }
236            CallError::Failure => {
237                write!(f, "ffi call returned nullptr instead of string")
238            }
239            CallError::NotUtf8(utf8_error) => {
240                write!(f, "ffi call returned non-utf8 string {utf8_error}")
241            }
242            CallError::InvalidJson(serde_error) => {
243                write!(f, "ffi call returned invalid JSON {serde_error}")
244            }
245        }
246    }
247}
248
249impl std::error::Error for CallError {
250    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
251        match self {
252            CallError::NullByteInput(error) => Some(error),
253            CallError::Failure => None,
254            CallError::NotUtf8(error) => Some(error),
255            CallError::InvalidJson(error) => Some(error),
256        }
257    }
258}