1#![allow(unsafe_code)]
6
7use serde_json::Value;
22use std::cell::RefCell;
23use std::ffi::{CStr, CString};
24use std::os::raw::{c_char, c_void};
25use webui_handler::plugin::fast_v2::FastV2HydrationPlugin;
26use webui_handler::plugin::fast_v3::FastV3HydrationPlugin;
27use webui_handler::plugin::webui::WebUIHydrationPlugin;
28use webui_handler::{Protocol, RenderOptions, ResponseWriter, WebUIHandler};
29
30#[allow(non_camel_case_types)]
32pub type webui_protocol_t = c_void;
33
34thread_local! {
39 static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
41}
42
43fn set_last_error(msg: impl Into<String>) {
45 let mut bytes = msg.into().into_bytes();
46 if let Some(nul_pos) = bytes.iter().position(|byte| *byte == 0) {
47 bytes.truncate(nul_pos);
48 }
49
50 let c_string = unsafe { CString::from_vec_unchecked(bytes) };
52 LAST_ERROR.with(|cell| {
53 cell.replace(Some(c_string));
54 });
55}
56
57fn clear_last_error() {
59 LAST_ERROR.with(|cell| {
60 cell.replace(None);
61 });
62}
63
64struct HandlerContext {
70 handler: WebUIHandler,
71 nonce: Option<String>,
73}
74
75struct ProtocolContext {
77 protocol: Protocol,
78}
79
80struct StringResponseWriter {
82 content: String,
83}
84
85impl StringResponseWriter {
86 fn new() -> Self {
87 Self {
88 content: String::new(),
89 }
90 }
91}
92
93impl ResponseWriter for StringResponseWriter {
94 fn write(&mut self, content: &str) -> webui_handler::Result<()> {
95 self.content.push_str(content);
96 Ok(())
97 }
98
99 fn end(&mut self) -> webui_handler::Result<()> {
100 Ok(())
101 }
102}
103
104#[no_mangle]
117pub extern "C" fn webui_last_error() -> *const c_char {
118 LAST_ERROR.with(|cell| {
119 let borrow = cell.borrow();
120 match borrow.as_ref() {
121 Some(c_str) => c_str.as_ptr(),
122 None => std::ptr::null(),
123 }
124 })
125}
126
127#[no_mangle]
136pub extern "C" fn webui_handler_create() -> *mut c_void {
137 let handler = WebUIHandler::new();
138 let context = Box::new(HandlerContext {
139 handler,
140 nonce: None,
141 });
142 Box::into_raw(context) as *mut c_void
143}
144
145#[no_mangle]
162pub unsafe extern "C" fn webui_handler_create_with_plugin(plugin_id: *const c_char) -> *mut c_void {
163 clear_last_error();
164
165 let handler = if plugin_id.is_null() {
166 WebUIHandler::new()
167 } else {
168 match CStr::from_ptr(plugin_id).to_str() {
169 Ok("fast" | "fast-v2") => {
170 WebUIHandler::with_plugin(|| Box::new(FastV2HydrationPlugin::new()))
171 }
172 Ok("fast-v3") => WebUIHandler::with_plugin(|| Box::new(FastV3HydrationPlugin::new())),
173 Ok("webui") => WebUIHandler::with_plugin(|| Box::new(WebUIHydrationPlugin::new())),
174 Ok(unknown) => {
175 set_last_error(format!(
176 "unknown plugin: {unknown}. Use \"webui\", \"fast-v3\", \"fast-v2\", or \"fast\"."
177 ));
178 return std::ptr::null_mut();
179 }
180 Err(e) => {
181 set_last_error(format!("invalid UTF-8 in plugin_id: {e}"));
182 return std::ptr::null_mut();
183 }
184 }
185 };
186
187 let context = Box::new(HandlerContext {
188 handler,
189 nonce: None,
190 });
191 Box::into_raw(context) as *mut c_void
192}
193
194#[no_mangle]
201pub unsafe extern "C" fn webui_handler_destroy(handler_ptr: *mut c_void) {
202 if !handler_ptr.is_null() {
203 let _ = Box::from_raw(handler_ptr as *mut HandlerContext);
204 }
205}
206
207#[no_mangle]
216pub unsafe extern "C" fn webui_protocol_create(
217 protocol_data: *const u8,
218 protocol_len: usize,
219) -> *mut webui_protocol_t {
220 clear_last_error();
221 if protocol_data.is_null() {
222 set_last_error("protocol_data is null");
223 return std::ptr::null_mut();
224 }
225
226 match std::panic::catch_unwind(|| {
227 let bytes = unsafe { std::slice::from_raw_parts(protocol_data, protocol_len) };
229 match Protocol::from_protobuf(bytes) {
230 Ok(protocol) => {
231 Box::into_raw(Box::new(ProtocolContext { protocol })) as *mut webui_protocol_t
232 }
233 Err(error) => {
234 set_last_error(format!("failed to parse protobuf protocol: {error}"));
235 std::ptr::null_mut()
236 }
237 }
238 }) {
239 Ok(ptr) => ptr,
240 Err(_) => {
241 set_last_error("panic in webui_protocol_create");
242 std::ptr::null_mut()
243 }
244 }
245}
246
247#[no_mangle]
254pub unsafe extern "C" fn webui_protocol_destroy(protocol_ptr: *mut webui_protocol_t) {
255 if !protocol_ptr.is_null() {
256 let _ = unsafe { Box::from_raw(protocol_ptr as *mut ProtocolContext) };
259 }
260}
261
262#[no_mangle]
282pub unsafe extern "C" fn webui_handler_set_nonce(handler_ptr: *mut c_void, nonce: *const c_char) {
283 clear_last_error();
284
285 if handler_ptr.is_null() {
286 set_last_error("handler_ptr is null");
287 return;
288 }
289
290 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
291 let context = &mut *(handler_ptr as *mut HandlerContext);
293
294 if nonce.is_null() {
295 context.nonce = None;
296 } else {
297 match CStr::from_ptr(nonce).to_str() {
299 Ok(s) => context.nonce = Some(s.to_string()),
300 Err(e) => {
301 set_last_error(format!("invalid UTF-8 in nonce: {e}"));
302 }
303 }
304 }
305 })) {
306 Ok(()) => {}
307 Err(_) => {
308 set_last_error("panic in webui_handler_set_nonce");
309 }
310 }
311}
312
313#[no_mangle]
325pub unsafe extern "C" fn webui_handler_render(
326 handler_ptr: *mut c_void,
327 protocol_ptr: *const webui_protocol_t,
328 data_json: *const c_char,
329 entry_id: *const c_char,
330 request_path: *const c_char,
331) -> *mut c_char {
332 clear_last_error();
333
334 match std::panic::catch_unwind(|| {
335 if handler_ptr.is_null()
336 || protocol_ptr.is_null()
337 || data_json.is_null()
338 || entry_id.is_null()
339 || request_path.is_null()
340 {
341 set_last_error("one or more required arguments are null");
342 return std::ptr::null_mut();
343 }
344
345 let context = unsafe { &*(handler_ptr as *const HandlerContext) };
347 let protocol_context = unsafe { &*(protocol_ptr as *const ProtocolContext) };
348 unsafe {
350 render_decoded_protocol(
351 context,
352 &protocol_context.protocol,
353 data_json,
354 entry_id,
355 request_path,
356 )
357 }
358 }) {
359 Ok(ptr) => ptr,
360 Err(_) => {
361 set_last_error("panic in webui_handler_render");
362 std::ptr::null_mut()
363 }
364 }
365}
366
367unsafe fn render_decoded_protocol(
368 context: &HandlerContext,
369 protocol: &Protocol,
370 data_json: *const c_char,
371 entry_id: *const c_char,
372 request_path: *const c_char,
373) -> *mut c_char {
374 let data_str = match unsafe { CStr::from_ptr(data_json) }.to_str() {
376 Ok(s) => s,
377 Err(e) => {
378 set_last_error(format!("invalid UTF-8 in data_json: {e}"));
379 return std::ptr::null_mut();
380 }
381 };
382 let entry_str = match unsafe { CStr::from_ptr(entry_id) }.to_str() {
384 Ok(s) => s,
385 Err(e) => {
386 set_last_error(format!("invalid UTF-8 in entry_id: {e}"));
387 return std::ptr::null_mut();
388 }
389 };
390 let path_str = match unsafe { CStr::from_ptr(request_path) }.to_str() {
392 Ok(s) => s,
393 Err(e) => {
394 set_last_error(format!("invalid UTF-8 in request_path: {e}"));
395 return std::ptr::null_mut();
396 }
397 };
398
399 let data: Value = match serde_json::from_str(data_str) {
400 Ok(d) => d,
401 Err(e) => {
402 set_last_error(format!("failed to parse data JSON: {e}"));
403 return std::ptr::null_mut();
404 }
405 };
406
407 let mut options = RenderOptions::new(entry_str, path_str);
409 if let Some(ref nonce) = context.nonce {
410 options = options.with_nonce(nonce);
411 }
412
413 let mut writer = StringResponseWriter::new();
414 match context
415 .handler
416 .render(protocol, &data, &options, &mut writer)
417 {
418 Ok(_) => match CString::new(writer.content) {
419 Ok(s) => s.into_raw(),
420 Err(e) => {
421 set_last_error(format!("rendered output contains interior NUL byte: {e}"));
422 std::ptr::null_mut()
423 }
424 },
425 Err(e) => {
426 set_last_error(format!("render failed: {e}"));
427 std::ptr::null_mut()
428 }
429 }
430}
431
432#[no_mangle]
443pub unsafe extern "C" fn webui_protocol_render_partial(
444 protocol_ptr: *const webui_protocol_t,
445 state_json: *const c_char,
446 entry_id: *const c_char,
447 request_path: *const c_char,
448 inventory_hex: *const c_char,
449) -> *mut c_char {
450 clear_last_error();
451
452 match std::panic::catch_unwind(|| {
453 if protocol_ptr.is_null()
454 || state_json.is_null()
455 || entry_id.is_null()
456 || request_path.is_null()
457 || inventory_hex.is_null()
458 {
459 set_last_error("one or more required arguments are null");
460 return std::ptr::null_mut();
461 }
462
463 let protocol_context = unsafe { &*(protocol_ptr as *const ProtocolContext) };
465 let state_str = match unsafe { CStr::from_ptr(state_json) }.to_str() {
466 Ok(value) => value,
467 Err(error) => {
468 set_last_error(format!("invalid UTF-8 in state_json: {error}"));
469 return std::ptr::null_mut();
470 }
471 };
472 let entry_str = match unsafe { CStr::from_ptr(entry_id) }.to_str() {
473 Ok(value) => value,
474 Err(error) => {
475 set_last_error(format!("invalid UTF-8 in entry_id: {error}"));
476 return std::ptr::null_mut();
477 }
478 };
479 let request_path_str = match unsafe { CStr::from_ptr(request_path) }.to_str() {
480 Ok(value) => value,
481 Err(error) => {
482 set_last_error(format!("invalid UTF-8 in request_path: {error}"));
483 return std::ptr::null_mut();
484 }
485 };
486 let inventory_str = match unsafe { CStr::from_ptr(inventory_hex) }.to_str() {
487 Ok(value) => value,
488 Err(error) => {
489 set_last_error(format!("invalid UTF-8 in inventory_hex: {error}"));
490 return std::ptr::null_mut();
491 }
492 };
493
494 let output = match protocol_context.protocol.render_partial(
495 state_str,
496 entry_str,
497 request_path_str,
498 inventory_str,
499 ) {
500 Ok(value) => value,
501 Err(error) => {
502 set_last_error(format!("render_partial failed: {error}"));
503 return std::ptr::null_mut();
504 }
505 };
506
507 match CString::new(output) {
508 Ok(value) => value.into_raw(),
509 Err(error) => {
510 set_last_error(format!("JSON output contains interior NUL byte: {error}"));
511 std::ptr::null_mut()
512 }
513 }
514 }) {
515 Ok(ptr) => ptr,
516 Err(_) => {
517 set_last_error("panic in webui_protocol_render_partial");
518 std::ptr::null_mut()
519 }
520 }
521}
522
523#[no_mangle]
530pub unsafe extern "C" fn webui_protocol_render_component_templates(
531 protocol_ptr: *const webui_protocol_t,
532 component_tags_json: *const c_char,
533 inventory_hex: *const c_char,
534) -> *mut c_char {
535 clear_last_error();
536
537 match std::panic::catch_unwind(|| {
538 if protocol_ptr.is_null() || component_tags_json.is_null() || inventory_hex.is_null() {
539 set_last_error("one or more required arguments are null");
540 return std::ptr::null_mut();
541 }
542
543 let protocol_context = unsafe { &*(protocol_ptr as *const ProtocolContext) };
545 let tags_str = match unsafe { CStr::from_ptr(component_tags_json) }.to_str() {
546 Ok(value) => value,
547 Err(error) => {
548 set_last_error(format!("invalid UTF-8 in component_tags_json: {error}"));
549 return std::ptr::null_mut();
550 }
551 };
552 let tags: Vec<String> = match serde_json::from_str(tags_str) {
553 Ok(value) => value,
554 Err(error) => {
555 set_last_error(format!("invalid tags JSON: {error}"));
556 return std::ptr::null_mut();
557 }
558 };
559 let tag_refs: Vec<&str> = tags.iter().map(String::as_str).collect();
560 let inventory_str = match unsafe { CStr::from_ptr(inventory_hex) }.to_str() {
561 Ok(value) => value,
562 Err(error) => {
563 set_last_error(format!("invalid UTF-8 in inventory_hex: {error}"));
564 return std::ptr::null_mut();
565 }
566 };
567
568 let result = match protocol_context
569 .protocol
570 .render_component_templates(&tag_refs, inventory_str)
571 {
572 Ok(value) => value,
573 Err(error) => {
574 set_last_error(format!("render_component_templates failed: {error}"));
575 return std::ptr::null_mut();
576 }
577 };
578
579 match CString::new(result.to_string()) {
580 Ok(value) => value.into_raw(),
581 Err(error) => {
582 set_last_error(format!("JSON output contains interior NUL byte: {error}"));
583 std::ptr::null_mut()
584 }
585 }
586 }) {
587 Ok(ptr) => ptr,
588 Err(_) => {
589 set_last_error("panic in webui_protocol_render_component_templates");
590 std::ptr::null_mut()
591 }
592 }
593}
594
595#[no_mangle]
603pub unsafe extern "C" fn webui_free(string_ptr: *mut c_char) {
604 if !string_ptr.is_null() {
605 let _ = CString::from_raw(string_ptr);
606 }
607}
608
609#[no_mangle]
618pub unsafe extern "C" fn webui_protocol_tokens(
619 protocol_ptr: *const webui_protocol_t,
620) -> *mut c_char {
621 clear_last_error();
622
623 match std::panic::catch_unwind(|| {
624 if protocol_ptr.is_null() {
625 set_last_error("protocol_ptr is null");
626 return std::ptr::null_mut();
627 }
628
629 let protocol_context = unsafe { &*(protocol_ptr as *const ProtocolContext) };
631 let joined = protocol_context.protocol.tokens().join("\n");
632
633 match CString::new(joined) {
634 Ok(value) => value.into_raw(),
635 Err(error) => {
636 set_last_error(format!("token string contains null byte: {error}"));
637 std::ptr::null_mut()
638 }
639 }
640 }) {
641 Ok(ptr) => ptr,
642 Err(_) => {
643 set_last_error("panic in webui_protocol_tokens");
644 std::ptr::null_mut()
645 }
646 }
647}