simploxide_sxcrt_sys/
lib.rs1use serde::Deserialize;
2
3use std::{
4 ffi::{CStr, CString, NulError, c_char, c_int, c_void},
5 sync::Once,
6};
7
8#[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 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 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 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}