1use std::ffi::{CStr, CString};
20use std::os::raw::{c_char, c_uint};
21use std::panic;
22use std::ptr;
23use std::sync::OnceLock;
24
25use crate::room_list::{
26 scrape as rs_scrape, scrape_with_proxy as rs_scrape_with_proxy,
27 RoomFilter, ScrapeOptions, ScrapeResult, UserFilter,
28};
29
30static RUNTIME: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
35
36fn runtime() -> &'static tokio::runtime::Runtime {
37 RUNTIME.get_or_init(|| {
38 tokio::runtime::Runtime::new()
39 .unwrap_or_else(|e| panic!("failed to create Tokio runtime: {}", e))
40 })
41}
42
43#[repr(C)]
48pub struct CRoomInfo {
49 pub room: c_uint,
50 pub max_connections: c_uint,
51 pub map_display: *mut c_char,
52 pub public_date: *mut c_char,
53 pub patrol: *mut c_char,
54 pub remarks: *mut c_char,
55}
56
57#[repr(C)]
58pub struct CLoggedInUser {
59 pub order: c_uint,
60 pub username: *mut c_char,
61 pub room: c_uint,
62 pub state: c_uint,
63}
64
65#[repr(C)]
74pub struct CScrapeResult {
75 pub rooms: *mut CRoomInfo,
76 pub rooms_len: usize,
77 rooms_cap: usize, pub users: *mut CLoggedInUser,
79 pub users_len: usize,
80 users_cap: usize, pub error_code: c_uint,
83}
84
85#[repr(C)]
86pub struct CRoomFilter {
87 pub room_enabled: c_uint,
88 pub room: c_uint,
89 pub room_min_enabled: c_uint,
90 pub room_min: c_uint,
91 pub room_max_enabled: c_uint,
92 pub room_max: c_uint,
93 pub min_max_conn_enabled: c_uint,
94 pub min_max_conn: c_uint,
95 pub max_max_conn_enabled: c_uint,
96 pub max_max_conn: c_uint,
97 pub map_display: *const c_char,
98 pub public_date: *const c_char,
99 pub public_date_contains: *const c_char,
100 pub patrol: *const c_char,
101 pub remarks: *const c_char,
102 pub remarks_contains: *const c_char,
103}
104
105#[repr(C)]
106pub struct CUserFilter {
107 pub order_enabled: c_uint,
108 pub order: c_uint,
109 pub order_min_enabled: c_uint,
110 pub order_min: c_uint,
111 pub order_max_enabled: c_uint,
112 pub order_max: c_uint,
113 pub username: *const c_char,
114 pub username_contains: *const c_char,
115 pub room_enabled: c_uint,
116 pub room: c_uint,
117 pub room_min_enabled: c_uint,
118 pub room_min: c_uint,
119 pub room_max_enabled: c_uint,
120 pub room_max: c_uint,
121 pub state_enabled: c_uint,
122 pub state: c_uint,
123}
124
125thread_local! {
130 static LAST_ERROR: std::cell::RefCell<CString> =
131 std::cell::RefCell::new(CString::new("").unwrap_or_default());
132}
133
134fn set_last_error(msg: &str) {
135 let c = CString::new(msg.replace('\0', "?")).unwrap_or_default();
136 LAST_ERROR.with(|e| *e.borrow_mut() = c);
137}
138
139#[no_mangle]
142pub extern "C" fn scraper_last_error() -> *const c_char {
143 LAST_ERROR.with(|e| e.borrow().as_ptr())
144}
145
146fn error_result(code: c_uint, msg: &str) -> *mut CScrapeResult {
152 set_last_error(msg);
153 Box::into_raw(Box::new(CScrapeResult {
154 rooms: ptr::null_mut(), rooms_len: 0, rooms_cap: 0,
155 users: ptr::null_mut(), users_len: 0, users_cap: 0,
156 error_code: code,
157 }))
158}
159
160fn cstr_opt(p: *const c_char) -> Option<String> {
165 if p.is_null() { return None; }
166 unsafe { CStr::from_ptr(p).to_str().ok().map(|s| s.to_string()) }
167}
168
169fn to_cstring(s: &str) -> *mut c_char {
170 CString::new(s.replace('\0', "?"))
171 .map(|c| c.into_raw())
172 .unwrap_or(ptr::null_mut())
173}
174
175fn enabled(flag: c_uint, val: u32) -> Option<u32> {
176 if flag != 0 { Some(val) } else { None }
177}
178
179fn c_room_filter(f: &CRoomFilter) -> RoomFilter {
180 RoomFilter {
181 room: enabled(f.room_enabled, f.room),
182 room_min: enabled(f.room_min_enabled, f.room_min),
183 room_max: enabled(f.room_max_enabled, f.room_max),
184 min_max_conn: enabled(f.min_max_conn_enabled, f.min_max_conn),
185 max_max_conn: enabled(f.max_max_conn_enabled, f.max_max_conn),
186 map_display: cstr_opt(f.map_display),
187 public_date: cstr_opt(f.public_date),
188 public_date_contains: cstr_opt(f.public_date_contains),
189 patrol: cstr_opt(f.patrol),
190 remarks: cstr_opt(f.remarks),
191 remarks_contains: cstr_opt(f.remarks_contains),
192 }
193}
194
195fn c_user_filter(f: &CUserFilter) -> UserFilter {
196 UserFilter {
197 order: enabled(f.order_enabled, f.order),
198 order_min: enabled(f.order_min_enabled, f.order_min),
199 order_max: enabled(f.order_max_enabled, f.order_max),
200 username: cstr_opt(f.username),
201 username_contains: cstr_opt(f.username_contains),
202 room: enabled(f.room_enabled, f.room),
203 room_min: enabled(f.room_min_enabled, f.room_min),
204 room_max: enabled(f.room_max_enabled, f.room_max),
205 state: enabled(f.state_enabled, f.state),
206 }
207}
208
209fn build_opts(
210 room_filter: *const CRoomFilter,
211 user_filter: *const CUserFilter,
212) -> ScrapeOptions {
213 let mut opts = ScrapeOptions::default();
214 if !room_filter.is_null() {
215 opts = opts.with_room_filter(c_room_filter(unsafe { &*room_filter }));
216 }
217 if !user_filter.is_null() {
218 opts = opts.with_user_filter(c_user_filter(unsafe { &*user_filter }));
219 }
220 opts
221}
222
223fn result_to_c(
228 res: Result<ScrapeResult, Box<dyn std::error::Error + Send + Sync>>,
229) -> *mut CScrapeResult {
230 match res {
231 Err(e) => error_result(1, &e.to_string()),
232 Ok(sr) => {
233 let mut c_rooms: Vec<CRoomInfo> = sr.rooms.iter().map(|r| CRoomInfo {
235 room: r.room,
236 max_connections: r.max_connections,
237 map_display: to_cstring(&r.map_display),
238 public_date: to_cstring(&r.public_date),
239 patrol: to_cstring(&r.patrol),
240 remarks: to_cstring(&r.remarks),
241 }).collect();
242 let rooms_len = c_rooms.len();
243 let rooms_cap = c_rooms.capacity();
244 let rooms_ptr = if rooms_len > 0 {
245 let p = c_rooms.as_mut_ptr();
246 std::mem::forget(c_rooms);
247 p
248 } else {
249 ptr::null_mut()
250 };
251
252 let (users_ptr, users_len, users_cap) = match sr.logged_in_users {
254 None => (ptr::null_mut(), 0, 0),
255 Some(users) => {
256 let mut c_users: Vec<CLoggedInUser> = users.iter().map(|u| CLoggedInUser {
257 order: u.order,
258 username: to_cstring(&u.username),
259 room: u.room,
260 state: u.state,
261 }).collect();
262 let len = c_users.len();
263 let cap = c_users.capacity();
264 let p = c_users.as_mut_ptr();
265 std::mem::forget(c_users);
266 (p, len, cap)
267 }
268 };
269
270 Box::into_raw(Box::new(CScrapeResult {
271 rooms: rooms_ptr, rooms_len, rooms_cap,
272 users: users_ptr, users_len, users_cap,
273 error_code: 0,
274 }))
275 }
276 }
277}
278
279unsafe fn cstr_required<'a>(
290 p: *const c_char,
291 name: &'static str,
292) -> Result<&'a str, String> {
293 if p.is_null() {
294 return Err(format!("argument `{}` must not be NULL", name));
295 }
296 CStr::from_ptr(p)
297 .to_str()
298 .map_err(|e| format!("argument `{}` is not valid UTF-8: {}", name, e))
299}
300
301#[no_mangle]
314pub extern "C" fn scraper_scrape(
315 user: *const c_char,
316 pass: *const c_char,
317 room_filter: *const CRoomFilter,
318 user_filter: *const CUserFilter,
319) -> *mut CScrapeResult {
320 let outcome = panic::catch_unwind(|| {
322 let user = unsafe { cstr_required(user, "user") };
324 let pass = unsafe { cstr_required(pass, "pass") };
325 let (user, pass) = match (user, pass) {
326 (Ok(u), Ok(p)) => (u, p),
327 (Err(e), _) | (_, Err(e)) => return error_result(2, &e),
328 };
329 let opts = build_opts(room_filter, user_filter);
330 let res = runtime().block_on(rs_scrape(user, pass, opts));
331 result_to_c(res)
332 });
333
334 match outcome {
335 Ok(ptr) => ptr,
336 Err(_) => error_result(3, "internal panic in scraper_scrape"),
337 }
338}
339
340#[no_mangle]
347pub extern "C" fn scraper_scrape_with_proxy(
348 user: *const c_char,
349 pass: *const c_char,
350 proxy_uri: *const c_char,
351 room_filter: *const CRoomFilter,
352 user_filter: *const CUserFilter,
353) -> *mut CScrapeResult {
354 let outcome = panic::catch_unwind(|| {
355 let user = unsafe { cstr_required(user, "user") };
356 let pass = unsafe { cstr_required(pass, "pass") };
357 let proxy_uri = unsafe { cstr_required(proxy_uri, "proxy_uri") };
358 let (user, pass, proxy_uri) = match (user, pass, proxy_uri) {
359 (Ok(u), Ok(p), Ok(x)) => (u, p, x),
360 (Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => return error_result(2, &e),
361 };
362 let opts = build_opts(room_filter, user_filter);
363 let res = runtime().block_on(rs_scrape_with_proxy(user, pass, proxy_uri, opts));
364 result_to_c(res)
365 });
366
367 match outcome {
368 Ok(ptr) => ptr,
369 Err(_) => error_result(3, "internal panic in scraper_scrape_with_proxy"),
370 }
371}
372
373#[no_mangle]
379pub extern "C" fn scraper_free_result(result: *mut CScrapeResult) {
380 if result.is_null() {
381 return;
382 }
383 let _ = panic::catch_unwind(|| {
384 unsafe {
385 let r = Box::from_raw(result);
386
387 if !r.rooms.is_null() {
388 let rooms = std::slice::from_raw_parts_mut(r.rooms, r.rooms_len);
389 for room in rooms.iter() {
390 if !room.map_display.is_null() { drop(CString::from_raw(room.map_display)); }
391 if !room.public_date.is_null() { drop(CString::from_raw(room.public_date)); }
392 if !room.patrol.is_null() { drop(CString::from_raw(room.patrol)); }
393 if !room.remarks.is_null() { drop(CString::from_raw(room.remarks)); }
394 }
395 drop(Vec::from_raw_parts(r.rooms, r.rooms_len, r.rooms_cap));
396 }
397
398 if !r.users.is_null() {
399 let users = std::slice::from_raw_parts_mut(r.users, r.users_len);
400 for user in users.iter() {
401 if !user.username.is_null() { drop(CString::from_raw(user.username)); }
402 }
403 drop(Vec::from_raw_parts(r.users, r.users_len, r.users_cap));
404 }
405 }
406 });
407}