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            bindings::chat_close_store(self.0);
96        }
97    }
98}
99
100#[derive(Debug, Clone, Copy)]
101pub enum MigrationConfirmation {
102    YesUp,
103    YesUpDown,
104    Console,
105    Error,
106}
107
108impl MigrationConfirmation {
109    fn as_cstr(&self) -> &'static CStr {
110        match self {
111            Self::YesUp => c"yesUp",
112            Self::YesUpDown => c"yesUpDown",
113            Self::Console => c"console",
114            Self::Error => c"error",
115        }
116    }
117}
118
119fn haskell_init() {
120    #[cfg(target_os = "windows")]
121    let args = Box::new([
122        c"simplex".as_ptr() as *mut c_char,
123        c"+RTS".as_ptr() as *mut c_char,
124        c"-A64m".as_ptr() as *mut c_char,
125        c"-H64m".as_ptr() as *mut c_char,
126        c"--install-signal-handlers=no".as_ptr() as *mut c_char,
127        std::ptr::null_mut(),
128    ]);
129
130    #[cfg(not(target_os = "windows"))]
131    let args = Box::new([
132        c"simplex".as_ptr() as *mut c_char,
133        c"+RTS".as_ptr() as *mut c_char,
134        c"-A64m".as_ptr() as *mut c_char,
135        c"-H64m".as_ptr() as *mut c_char,
136        c"-xn".as_ptr() as *mut c_char,
137        c"--install-signal-handlers=no".as_ptr() as *mut c_char,
138        std::ptr::null_mut(),
139    ]);
140
141    let mut argc: c_int = (args.len() - 1) as c_int;
142    let mut pargv: *mut *mut c_char = Box::leak(args).as_mut_ptr();
143
144    unsafe {
145        bindings::hs_init_with_rtsopts(&mut argc, &mut pargv);
146    }
147}
148
149fn c_res_to_string(c_res: &mut *mut c_char) -> Result<String, CallError> {
150    fn try_parse_c_res(c_res: *mut c_char) -> Result<String, CallError> {
151        if c_res.is_null() {
152            return Err(CallError::Failure);
153        }
154
155        // SAFETY:
156        // * SimpleX-Core-FFI functions should return valid null-terminated C strings
157        // * c_res ptr is not null(checked above)
158        // * c_res memory is not mutating and is hold exclusively while CStr::from_ptr borrow is
159        //   active(ensured by &mut in the outer method)
160        let string = unsafe { CStr::from_ptr(c_res).to_str()?.to_owned() };
161        Ok(string)
162    }
163
164    let parsed = try_parse_c_res(*c_res);
165
166    unsafe {
167        libc::free(*c_res as *mut c_void);
168    }
169    *c_res = std::ptr::null_mut();
170
171    parsed
172}
173
174#[derive(Debug)]
175pub enum InitError {
176    CallError(CallError),
177    DbError(serde_json::Value),
178}
179
180impl From<CallError> for InitError {
181    fn from(value: CallError) -> Self {
182        Self::CallError(value)
183    }
184}
185
186impl std::fmt::Display for InitError {
187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188        match self {
189            InitError::CallError(call_error) => call_error.fmt(f),
190            InitError::DbError(value) => {
191                write!(f, "cannot create DB connection:\n{value:#}")
192            }
193        }
194    }
195}
196
197impl std::error::Error for InitError {
198    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
199        match self {
200            Self::CallError(call_error) => Some(call_error),
201            Self::DbError(_) => None,
202        }
203    }
204}
205
206#[derive(Debug)]
207pub enum CallError {
208    NullByteInput(NulError),
209    Failure,
210    NotUtf8(std::str::Utf8Error),
211    InvalidJson(serde_json::Error),
212}
213
214impl From<NulError> for CallError {
215    fn from(value: NulError) -> Self {
216        Self::NullByteInput(value)
217    }
218}
219
220impl From<std::str::Utf8Error> for CallError {
221    fn from(value: std::str::Utf8Error) -> Self {
222        Self::NotUtf8(value)
223    }
224}
225
226impl From<serde_json::Error> for CallError {
227    fn from(value: serde_json::Error) -> Self {
228        Self::InvalidJson(value)
229    }
230}
231
232impl std::fmt::Display for CallError {
233    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234        match self {
235            CallError::NullByteInput(error) => {
236                write!(f, "null byte injection in one of the input strings {error}")
237            }
238            CallError::Failure => {
239                write!(f, "ffi call returned nullptr instead of string")
240            }
241            CallError::NotUtf8(utf8_error) => {
242                write!(f, "ffi call returned non-utf8 string {utf8_error}")
243            }
244            CallError::InvalidJson(serde_error) => {
245                write!(f, "ffi call returned invalid JSON {serde_error}")
246            }
247        }
248    }
249}
250
251impl std::error::Error for CallError {
252    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
253        match self {
254            CallError::NullByteInput(error) => Some(error),
255            CallError::Failure => None,
256            CallError::NotUtf8(error) => Some(error),
257            CallError::InvalidJson(error) => Some(error),
258        }
259    }
260}