Skip to main content

simploxide_sxcrt_sys/
lib.rs

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