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 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 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}