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 std::sync::Arc;
26use webui_handler::plugin::fast_v2::FastV2HydrationPlugin;
27use webui_handler::plugin::fast_v3::FastV3HydrationPlugin;
28use webui_handler::plugin::webui::WebUIHydrationPlugin;
29use webui_handler::{
30 BoundaryId, BoundaryMode, Protocol, RenderOptions, ResponseWriter, SessionOptions,
31 StreamingSession, WebUIHandler,
32};
33
34#[allow(non_camel_case_types)]
36pub type webui_protocol_t = c_void;
37
38thread_local! {
43 static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
45}
46
47fn set_last_error(msg: impl Into<String>) {
49 let mut bytes = msg.into().into_bytes();
50 if let Some(nul_pos) = bytes.iter().position(|byte| *byte == 0) {
51 bytes.truncate(nul_pos);
52 }
53
54 let c_string = unsafe { CString::from_vec_unchecked(bytes) };
56 LAST_ERROR.with(|cell| {
57 cell.replace(Some(c_string));
58 });
59}
60
61fn clear_last_error() {
63 LAST_ERROR.with(|cell| {
64 cell.replace(None);
65 });
66}
67
68struct HandlerContext {
74 handler: Arc<WebUIHandler>,
75 nonce: Option<String>,
77}
78
79struct ProtocolContext {
81 protocol: Arc<Protocol>,
82}
83
84struct StringResponseWriter {
86 content: String,
87}
88
89impl StringResponseWriter {
90 fn new() -> Self {
91 Self {
92 content: String::new(),
93 }
94 }
95}
96
97impl ResponseWriter for StringResponseWriter {
98 fn write(&mut self, content: &str) -> webui_handler::Result<()> {
99 self.content.push_str(content);
100 Ok(())
101 }
102
103 fn end(&mut self) -> webui_handler::Result<()> {
104 Ok(())
105 }
106}
107
108#[no_mangle]
121pub extern "C" fn webui_last_error() -> *const c_char {
122 LAST_ERROR.with(|cell| {
123 let borrow = cell.borrow();
124 match borrow.as_ref() {
125 Some(c_str) => c_str.as_ptr(),
126 None => std::ptr::null(),
127 }
128 })
129}
130
131#[no_mangle]
140pub extern "C" fn webui_handler_create() -> *mut c_void {
141 let handler = WebUIHandler::new();
142 let context = Box::new(HandlerContext {
143 handler: Arc::new(handler),
144 nonce: None,
145 });
146 Box::into_raw(context) as *mut c_void
147}
148
149#[no_mangle]
166pub unsafe extern "C" fn webui_handler_create_with_plugin(plugin_id: *const c_char) -> *mut c_void {
167 clear_last_error();
168
169 let handler = if plugin_id.is_null() {
170 WebUIHandler::new()
171 } else {
172 match CStr::from_ptr(plugin_id).to_str() {
173 Ok("fast" | "fast-v2") => {
174 WebUIHandler::with_plugin(|| Box::new(FastV2HydrationPlugin::new()))
175 }
176 Ok("fast-v3") => WebUIHandler::with_plugin(|| Box::new(FastV3HydrationPlugin::new())),
177 Ok("webui") => WebUIHandler::with_plugin(|| Box::new(WebUIHydrationPlugin::new())),
178 Ok(unknown) => {
179 set_last_error(format!(
180 "unknown plugin: {unknown}. Use \"webui\", \"fast-v3\", \"fast-v2\", or \"fast\"."
181 ));
182 return std::ptr::null_mut();
183 }
184 Err(e) => {
185 set_last_error(format!("invalid UTF-8 in plugin_id: {e}"));
186 return std::ptr::null_mut();
187 }
188 }
189 };
190
191 let context = Box::new(HandlerContext {
192 handler: Arc::new(handler),
193 nonce: None,
194 });
195 Box::into_raw(context) as *mut c_void
196}
197
198#[no_mangle]
205pub unsafe extern "C" fn webui_handler_destroy(handler_ptr: *mut c_void) {
206 if !handler_ptr.is_null() {
207 let _ = Box::from_raw(handler_ptr as *mut HandlerContext);
208 }
209}
210
211#[no_mangle]
220pub unsafe extern "C" fn webui_protocol_create(
221 protocol_data: *const u8,
222 protocol_len: usize,
223) -> *mut webui_protocol_t {
224 clear_last_error();
225 if protocol_data.is_null() {
226 set_last_error("protocol_data is null");
227 return std::ptr::null_mut();
228 }
229
230 match std::panic::catch_unwind(|| {
231 let bytes = unsafe { std::slice::from_raw_parts(protocol_data, protocol_len) };
233 match Protocol::from_protobuf(bytes) {
234 Ok(protocol) => Box::into_raw(Box::new(ProtocolContext {
235 protocol: Arc::new(protocol),
236 })) as *mut webui_protocol_t,
237 Err(error) => {
238 set_last_error(format!("failed to parse protobuf protocol: {error}"));
239 std::ptr::null_mut()
240 }
241 }
242 }) {
243 Ok(ptr) => ptr,
244 Err(_) => {
245 set_last_error("panic in webui_protocol_create");
246 std::ptr::null_mut()
247 }
248 }
249}
250
251#[no_mangle]
258pub unsafe extern "C" fn webui_protocol_destroy(protocol_ptr: *mut webui_protocol_t) {
259 if !protocol_ptr.is_null() {
260 let _ = unsafe { Box::from_raw(protocol_ptr as *mut ProtocolContext) };
263 }
264}
265
266#[no_mangle]
286pub unsafe extern "C" fn webui_handler_set_nonce(handler_ptr: *mut c_void, nonce: *const c_char) {
287 clear_last_error();
288
289 if handler_ptr.is_null() {
290 set_last_error("handler_ptr is null");
291 return;
292 }
293
294 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
295 let context = &mut *(handler_ptr as *mut HandlerContext);
297
298 if nonce.is_null() {
299 context.nonce = None;
300 } else {
301 match CStr::from_ptr(nonce).to_str() {
303 Ok(s) => context.nonce = Some(s.to_string()),
304 Err(e) => {
305 set_last_error(format!("invalid UTF-8 in nonce: {e}"));
306 }
307 }
308 }
309 })) {
310 Ok(()) => {}
311 Err(_) => {
312 set_last_error("panic in webui_handler_set_nonce");
313 }
314 }
315}
316
317#[no_mangle]
329pub unsafe extern "C" fn webui_handler_render(
330 handler_ptr: *mut c_void,
331 protocol_ptr: *const webui_protocol_t,
332 data_json: *const c_char,
333 entry_id: *const c_char,
334 request_path: *const c_char,
335) -> *mut c_char {
336 clear_last_error();
337
338 match std::panic::catch_unwind(|| {
339 if handler_ptr.is_null()
340 || protocol_ptr.is_null()
341 || data_json.is_null()
342 || entry_id.is_null()
343 || request_path.is_null()
344 {
345 set_last_error("one or more required arguments are null");
346 return std::ptr::null_mut();
347 }
348
349 let context = unsafe { &*(handler_ptr as *const HandlerContext) };
351 let protocol_context = unsafe { &*(protocol_ptr as *const ProtocolContext) };
352 unsafe {
354 render_decoded_protocol(
355 context,
356 &protocol_context.protocol,
357 data_json,
358 entry_id,
359 request_path,
360 )
361 }
362 }) {
363 Ok(ptr) => ptr,
364 Err(_) => {
365 set_last_error("panic in webui_handler_render");
366 std::ptr::null_mut()
367 }
368 }
369}
370
371unsafe fn render_decoded_protocol(
372 context: &HandlerContext,
373 protocol: &Protocol,
374 data_json: *const c_char,
375 entry_id: *const c_char,
376 request_path: *const c_char,
377) -> *mut c_char {
378 let data_str = match unsafe { CStr::from_ptr(data_json) }.to_str() {
380 Ok(s) => s,
381 Err(e) => {
382 set_last_error(format!("invalid UTF-8 in data_json: {e}"));
383 return std::ptr::null_mut();
384 }
385 };
386 let entry_str = match unsafe { CStr::from_ptr(entry_id) }.to_str() {
388 Ok(s) => s,
389 Err(e) => {
390 set_last_error(format!("invalid UTF-8 in entry_id: {e}"));
391 return std::ptr::null_mut();
392 }
393 };
394 let path_str = match unsafe { CStr::from_ptr(request_path) }.to_str() {
396 Ok(s) => s,
397 Err(e) => {
398 set_last_error(format!("invalid UTF-8 in request_path: {e}"));
399 return std::ptr::null_mut();
400 }
401 };
402
403 let data: Value = match serde_json::from_str(data_str) {
404 Ok(d) => d,
405 Err(e) => {
406 set_last_error(format!("failed to parse data JSON: {e}"));
407 return std::ptr::null_mut();
408 }
409 };
410
411 let mut options = RenderOptions::new(entry_str, path_str);
413 if let Some(ref nonce) = context.nonce {
414 options = options.with_nonce(nonce);
415 }
416
417 let mut writer = StringResponseWriter::new();
418 match context
419 .handler
420 .render(protocol, &data, &options, &mut writer)
421 {
422 Ok(_) => match CString::new(writer.content) {
423 Ok(s) => s.into_raw(),
424 Err(e) => {
425 set_last_error(format!("rendered output contains interior NUL byte: {e}"));
426 std::ptr::null_mut()
427 }
428 },
429 Err(e) => {
430 set_last_error(format!("render failed: {e}"));
431 std::ptr::null_mut()
432 }
433 }
434}
435
436#[no_mangle]
447pub unsafe extern "C" fn webui_protocol_render_partial(
448 protocol_ptr: *const webui_protocol_t,
449 state_json: *const c_char,
450 entry_id: *const c_char,
451 request_path: *const c_char,
452 inventory_hex: *const c_char,
453) -> *mut c_char {
454 clear_last_error();
455
456 match std::panic::catch_unwind(|| {
457 if protocol_ptr.is_null()
458 || state_json.is_null()
459 || entry_id.is_null()
460 || request_path.is_null()
461 || inventory_hex.is_null()
462 {
463 set_last_error("one or more required arguments are null");
464 return std::ptr::null_mut();
465 }
466
467 let protocol_context = unsafe { &*(protocol_ptr as *const ProtocolContext) };
469 let state_str = match unsafe { CStr::from_ptr(state_json) }.to_str() {
470 Ok(value) => value,
471 Err(error) => {
472 set_last_error(format!("invalid UTF-8 in state_json: {error}"));
473 return std::ptr::null_mut();
474 }
475 };
476 let entry_str = match unsafe { CStr::from_ptr(entry_id) }.to_str() {
477 Ok(value) => value,
478 Err(error) => {
479 set_last_error(format!("invalid UTF-8 in entry_id: {error}"));
480 return std::ptr::null_mut();
481 }
482 };
483 let request_path_str = match unsafe { CStr::from_ptr(request_path) }.to_str() {
484 Ok(value) => value,
485 Err(error) => {
486 set_last_error(format!("invalid UTF-8 in request_path: {error}"));
487 return std::ptr::null_mut();
488 }
489 };
490 let inventory_str = match unsafe { CStr::from_ptr(inventory_hex) }.to_str() {
491 Ok(value) => value,
492 Err(error) => {
493 set_last_error(format!("invalid UTF-8 in inventory_hex: {error}"));
494 return std::ptr::null_mut();
495 }
496 };
497
498 let output = match protocol_context.protocol.render_partial(
499 state_str,
500 entry_str,
501 request_path_str,
502 inventory_str,
503 ) {
504 Ok(value) => value,
505 Err(error) => {
506 set_last_error(format!("render_partial failed: {error}"));
507 return std::ptr::null_mut();
508 }
509 };
510
511 match CString::new(output) {
512 Ok(value) => value.into_raw(),
513 Err(error) => {
514 set_last_error(format!("JSON output contains interior NUL byte: {error}"));
515 std::ptr::null_mut()
516 }
517 }
518 }) {
519 Ok(ptr) => ptr,
520 Err(_) => {
521 set_last_error("panic in webui_protocol_render_partial");
522 std::ptr::null_mut()
523 }
524 }
525}
526
527#[no_mangle]
534pub unsafe extern "C" fn webui_protocol_render_component_templates(
535 protocol_ptr: *const webui_protocol_t,
536 component_tags_json: *const c_char,
537 inventory_hex: *const c_char,
538) -> *mut c_char {
539 clear_last_error();
540
541 match std::panic::catch_unwind(|| {
542 if protocol_ptr.is_null() || component_tags_json.is_null() || inventory_hex.is_null() {
543 set_last_error("one or more required arguments are null");
544 return std::ptr::null_mut();
545 }
546
547 let protocol_context = unsafe { &*(protocol_ptr as *const ProtocolContext) };
549 let tags_str = match unsafe { CStr::from_ptr(component_tags_json) }.to_str() {
550 Ok(value) => value,
551 Err(error) => {
552 set_last_error(format!("invalid UTF-8 in component_tags_json: {error}"));
553 return std::ptr::null_mut();
554 }
555 };
556 let tags: Vec<String> = match serde_json::from_str(tags_str) {
557 Ok(value) => value,
558 Err(error) => {
559 set_last_error(format!("invalid tags JSON: {error}"));
560 return std::ptr::null_mut();
561 }
562 };
563 let tag_refs: Vec<&str> = tags.iter().map(String::as_str).collect();
564 let inventory_str = match unsafe { CStr::from_ptr(inventory_hex) }.to_str() {
565 Ok(value) => value,
566 Err(error) => {
567 set_last_error(format!("invalid UTF-8 in inventory_hex: {error}"));
568 return std::ptr::null_mut();
569 }
570 };
571
572 let result = match protocol_context
573 .protocol
574 .render_component_templates(&tag_refs, inventory_str)
575 {
576 Ok(value) => value,
577 Err(error) => {
578 set_last_error(format!("render_component_templates failed: {error}"));
579 return std::ptr::null_mut();
580 }
581 };
582
583 match CString::new(result.to_string()) {
584 Ok(value) => value.into_raw(),
585 Err(error) => {
586 set_last_error(format!("JSON output contains interior NUL byte: {error}"));
587 std::ptr::null_mut()
588 }
589 }
590 }) {
591 Ok(ptr) => ptr,
592 Err(_) => {
593 set_last_error("panic in webui_protocol_render_component_templates");
594 std::ptr::null_mut()
595 }
596 }
597}
598
599#[no_mangle]
607pub unsafe extern "C" fn webui_free(string_ptr: *mut c_char) {
608 if !string_ptr.is_null() {
609 let _ = CString::from_raw(string_ptr);
610 }
611}
612
613#[no_mangle]
622pub unsafe extern "C" fn webui_protocol_tokens(
623 protocol_ptr: *const webui_protocol_t,
624) -> *mut c_char {
625 clear_last_error();
626
627 match std::panic::catch_unwind(|| {
628 if protocol_ptr.is_null() {
629 set_last_error("protocol_ptr is null");
630 return std::ptr::null_mut();
631 }
632
633 let protocol_context = unsafe { &*(protocol_ptr as *const ProtocolContext) };
635 let joined = protocol_context.protocol.tokens().join("\n");
636
637 match CString::new(joined) {
638 Ok(value) => value.into_raw(),
639 Err(error) => {
640 set_last_error(format!("token string contains null byte: {error}"));
641 std::ptr::null_mut()
642 }
643 }
644 }) {
645 Ok(ptr) => ptr,
646 Err(_) => {
647 set_last_error("panic in webui_protocol_tokens");
648 std::ptr::null_mut()
649 }
650 }
651}
652
653#[allow(non_camel_case_types)]
659pub type webui_streaming_session_t = c_void;
660
661struct StreamingSessionContext {
663 session: StreamingSession,
664}
665
666#[no_mangle]
689pub unsafe extern "C" fn webui_streaming_session_create(
690 handler_ptr: *mut c_void,
691 protocol_ptr: *const webui_protocol_t,
692 entry_id: *const c_char,
693 request_path: *const c_char,
694) -> *mut webui_streaming_session_t {
695 clear_last_error();
696
697 match std::panic::catch_unwind(|| {
698 if handler_ptr.is_null()
699 || protocol_ptr.is_null()
700 || entry_id.is_null()
701 || request_path.is_null()
702 {
703 set_last_error("one or more required arguments are null");
704 return std::ptr::null_mut();
705 }
706
707 let handler_context = unsafe { &*(handler_ptr as *const HandlerContext) };
709 let protocol_context = unsafe { &*(protocol_ptr as *const ProtocolContext) };
710
711 let Some(entry) = (unsafe { utf8_arg(entry_id, "entry_id") }) else {
713 return std::ptr::null_mut();
714 };
715 let Some(path) = (unsafe { utf8_arg(request_path, "request_path") }) else {
717 return std::ptr::null_mut();
718 };
719
720 let mut options = SessionOptions::new(entry, path);
721 options.nonce = handler_context.nonce.clone();
722
723 match StreamingSession::new(
724 Arc::clone(&handler_context.handler),
725 Arc::clone(&protocol_context.protocol),
726 options,
727 ) {
728 Ok(session) => Box::into_raw(Box::new(StreamingSessionContext { session }))
729 as *mut webui_streaming_session_t,
730 Err(error) => {
731 set_last_error(error.to_string());
732 std::ptr::null_mut()
733 }
734 }
735 }) {
736 Ok(ptr) => ptr,
737 Err(_) => {
738 set_last_error("panic in webui_streaming_session_create");
739 std::ptr::null_mut()
740 }
741 }
742}
743
744#[no_mangle]
754pub unsafe extern "C" fn webui_streaming_session_destroy(
755 session_ptr: *mut webui_streaming_session_t,
756) {
757 if !session_ptr.is_null() {
758 let _ = unsafe { Box::from_raw(session_ptr as *mut StreamingSessionContext) };
761 }
762}
763
764#[no_mangle]
779pub unsafe extern "C" fn webui_streaming_session_boundary(
780 session_ptr: *const webui_streaming_session_t,
781 name: *const c_char,
782 out_boundary: *mut u32,
783) -> bool {
784 clear_last_error();
785
786 match std::panic::catch_unwind(|| {
787 if session_ptr.is_null() || name.is_null() || out_boundary.is_null() {
788 set_last_error("one or more required arguments are null");
789 return false;
790 }
791
792 let context = unsafe { &*(session_ptr as *const StreamingSessionContext) };
794 let Some(name) = (unsafe { utf8_arg(name, "name") }) else {
796 return false;
797 };
798
799 match context.session.boundary(name) {
800 Ok(boundary) => {
801 unsafe { *out_boundary = boundary.raw() };
803 true
804 }
805 Err(error) => {
806 set_last_error(error.to_string());
807 false
808 }
809 }
810 }) {
811 Ok(ok) => ok,
812 Err(_) => {
813 set_last_error("panic in webui_streaming_session_boundary");
814 false
815 }
816 }
817}
818
819#[no_mangle]
827pub unsafe extern "C" fn webui_streaming_session_boundary_count(
828 session_ptr: *const webui_streaming_session_t,
829) -> u32 {
830 if session_ptr.is_null() {
831 return 0;
832 }
833 let context = unsafe { &*(session_ptr as *const StreamingSessionContext) };
835 u32::try_from(context.session.boundary_count()).unwrap_or(u32::MAX)
836}
837
838#[no_mangle]
847pub unsafe extern "C" fn webui_streaming_session_is_finished(
848 session_ptr: *const webui_streaming_session_t,
849) -> bool {
850 if session_ptr.is_null() {
851 return true;
852 }
853 let context = unsafe { &*(session_ptr as *const StreamingSessionContext) };
855 context.session.is_finished()
856}
857
858#[no_mangle]
871pub unsafe extern "C" fn webui_streaming_session_write_shell(
872 session_ptr: *mut webui_streaming_session_t,
873 state_json: *const c_char,
874 out_len: *mut usize,
875) -> *mut c_char {
876 unsafe {
878 streaming_chunk_call(
879 session_ptr,
880 state_json,
881 out_len,
882 "webui_streaming_session_write_shell",
883 |session, state| session.write_shell(state),
884 )
885 }
886}
887
888#[no_mangle]
904pub unsafe extern "C" fn webui_streaming_session_write_boundary(
905 session_ptr: *mut webui_streaming_session_t,
906 boundary: u32,
907 state_json: *const c_char,
908 updatable: bool,
909 out_len: *mut usize,
910) -> *mut c_char {
911 let mode = if updatable {
912 BoundaryMode::Updatable
913 } else {
914 BoundaryMode::Final
915 };
916 unsafe {
918 streaming_chunk_call(
919 session_ptr,
920 state_json,
921 out_len,
922 "webui_streaming_session_write_boundary",
923 |session, state| session.write_boundary(BoundaryId::from_raw(boundary), state, mode),
924 )
925 }
926}
927
928#[no_mangle]
940pub unsafe extern "C" fn webui_streaming_session_update(
941 session_ptr: *mut webui_streaming_session_t,
942 boundary: u32,
943 state_json: *const c_char,
944 out_len: *mut usize,
945) -> *mut c_char {
946 unsafe {
948 streaming_chunk_call(
949 session_ptr,
950 state_json,
951 out_len,
952 "webui_streaming_session_update",
953 |session, state| session.update(BoundaryId::from_raw(boundary), state),
954 )
955 }
956}
957
958#[no_mangle]
973pub unsafe extern "C" fn webui_streaming_session_finish(
974 session_ptr: *mut webui_streaming_session_t,
975 state_json: *const c_char,
976 out_len: *mut usize,
977) -> *mut c_char {
978 unsafe {
980 streaming_chunk_call(
981 session_ptr,
982 state_json,
983 out_len,
984 "webui_streaming_session_finish",
985 |session, state| session.finish(state),
986 )
987 }
988}
989
990unsafe fn streaming_chunk_call(
996 session_ptr: *mut webui_streaming_session_t,
997 state_json: *const c_char,
998 out_len: *mut usize,
999 operation: &str,
1000 render: impl FnOnce(&mut StreamingSession, &Value) -> webui_handler::Result<Vec<u8>>,
1001) -> *mut c_char {
1002 clear_last_error();
1003
1004 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1005 if session_ptr.is_null() || state_json.is_null() {
1006 set_last_error("one or more required arguments are null");
1007 return std::ptr::null_mut();
1008 }
1009
1010 let context = unsafe { &mut *(session_ptr as *mut StreamingSessionContext) };
1012 let Some(state_str) = (unsafe { utf8_arg(state_json, "state_json") }) else {
1014 return std::ptr::null_mut();
1015 };
1016
1017 let state: Value = match serde_json::from_str(state_str) {
1018 Ok(value) => value,
1019 Err(error) => {
1020 set_last_error(format!("failed to parse state JSON: {error}"));
1021 return std::ptr::null_mut();
1022 }
1023 };
1024
1025 let bytes = match render(&mut context.session, &state) {
1026 Ok(bytes) => bytes,
1027 Err(error) => {
1028 set_last_error(error.to_string());
1029 return std::ptr::null_mut();
1030 }
1031 };
1032
1033 let length = bytes.len();
1034 match CString::new(bytes) {
1035 Ok(chunk) => {
1036 if !out_len.is_null() {
1037 unsafe { *out_len = length };
1039 }
1040 chunk.into_raw()
1041 }
1042 Err(error) => {
1043 set_last_error(format!("chunk contains interior NUL byte: {error}"));
1044 std::ptr::null_mut()
1045 }
1046 }
1047 })) {
1048 Ok(ptr) => ptr,
1049 Err(_) => {
1050 set_last_error(format!("panic in {operation}"));
1051 std::ptr::null_mut()
1052 }
1053 }
1054}
1055
1056unsafe fn utf8_arg<'a>(value: *const c_char, name: &str) -> Option<&'a str> {
1062 match unsafe { CStr::from_ptr(value) }.to_str() {
1064 Ok(text) => Some(text),
1065 Err(error) => {
1066 set_last_error(format!("invalid UTF-8 in {name}: {error}"));
1067 None
1068 }
1069 }
1070}