simploxide_sxcrt_sys/
lib.rs1#![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#[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 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 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 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}