1#![allow(unsafe_code)]
6
7use serde_json::Value;
34use std::cell::RefCell;
35use std::ffi::{CStr, CString};
36use std::os::raw::{c_char, c_void};
37use webui_handler::plugin::fast::FastHydrationPlugin;
38use webui_handler::plugin::webui::WebUIHydrationPlugin;
39use webui_handler::{RenderOptions, ResponseWriter, WebUIHandler};
40#[cfg(feature = "parser")]
41use webui_parser::HtmlParser;
42use webui_protocol::WebUIProtocol;
43
44thread_local! {
49 static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
51}
52
53fn set_last_error(msg: impl Into<String>) {
55 let mut bytes = msg.into().into_bytes();
56 if let Some(nul_pos) = bytes.iter().position(|byte| *byte == 0) {
57 bytes.truncate(nul_pos);
58 }
59
60 let c_string = unsafe { CString::from_vec_unchecked(bytes) };
62 LAST_ERROR.with(|cell| {
63 cell.replace(Some(c_string));
64 });
65}
66
67fn clear_last_error() {
69 LAST_ERROR.with(|cell| {
70 cell.replace(None);
71 });
72}
73
74struct HandlerContext {
80 handler: WebUIHandler,
81}
82
83struct StringResponseWriter {
85 content: String,
86}
87
88impl StringResponseWriter {
89 fn new() -> Self {
90 Self {
91 content: String::new(),
92 }
93 }
94}
95
96impl ResponseWriter for StringResponseWriter {
97 fn write(&mut self, content: &str) -> webui_handler::Result<()> {
98 self.content.push_str(content);
99 Ok(())
100 }
101
102 fn end(&mut self) -> webui_handler::Result<()> {
103 Ok(())
104 }
105}
106
107#[no_mangle]
120pub extern "C" fn webui_last_error() -> *const c_char {
121 LAST_ERROR.with(|cell| {
122 let borrow = cell.borrow();
123 match borrow.as_ref() {
124 Some(c_str) => c_str.as_ptr(),
125 None => std::ptr::null(),
126 }
127 })
128}
129
130#[no_mangle]
139pub extern "C" fn webui_handler_create() -> *mut c_void {
140 let handler = WebUIHandler::new();
141 let context = Box::new(HandlerContext { handler });
142 Box::into_raw(context) as *mut c_void
143}
144
145#[no_mangle]
160pub unsafe extern "C" fn webui_handler_create_with_plugin(plugin_id: *const c_char) -> *mut c_void {
161 clear_last_error();
162
163 let handler = if plugin_id.is_null() {
164 WebUIHandler::new()
165 } else {
166 match CStr::from_ptr(plugin_id).to_str() {
167 Ok("fast") => WebUIHandler::with_plugin(|| Box::new(FastHydrationPlugin::new())),
168 Ok("webui") => WebUIHandler::with_plugin(|| Box::new(WebUIHydrationPlugin::new())),
169 Ok(unknown) => {
170 set_last_error(format!("unknown plugin: {unknown}"));
171 return std::ptr::null_mut();
172 }
173 Err(e) => {
174 set_last_error(format!("invalid UTF-8 in plugin_id: {e}"));
175 return std::ptr::null_mut();
176 }
177 }
178 };
179
180 let context = Box::new(HandlerContext { handler });
181 Box::into_raw(context) as *mut c_void
182}
183
184#[no_mangle]
191pub unsafe extern "C" fn webui_handler_destroy(handler_ptr: *mut c_void) {
192 if !handler_ptr.is_null() {
193 let _ = Box::from_raw(handler_ptr as *mut HandlerContext);
194 }
195}
196
197#[no_mangle]
225pub unsafe extern "C" fn webui_handler_render(
226 handler_ptr: *mut c_void,
227 protocol_data: *const u8,
228 protocol_len: usize,
229 data_json: *const c_char,
230 entry_id: *const c_char,
231 request_path: *const c_char,
232) -> *mut c_char {
233 clear_last_error();
234
235 if handler_ptr.is_null()
236 || protocol_data.is_null()
237 || data_json.is_null()
238 || entry_id.is_null()
239 || request_path.is_null()
240 {
241 set_last_error("one or more required arguments are null");
242 return std::ptr::null_mut();
243 }
244
245 let context = &*(handler_ptr as *const HandlerContext);
247
248 let protocol_bytes = std::slice::from_raw_parts(protocol_data, protocol_len);
250
251 let data_str = match CStr::from_ptr(data_json).to_str() {
253 Ok(s) => s,
254 Err(e) => {
255 set_last_error(format!("invalid UTF-8 in data_json: {e}"));
256 return std::ptr::null_mut();
257 }
258 };
259
260 let entry_str = match CStr::from_ptr(entry_id).to_str() {
262 Ok(s) => s,
263 Err(e) => {
264 set_last_error(format!("invalid UTF-8 in entry_id: {e}"));
265 return std::ptr::null_mut();
266 }
267 };
268
269 let path_str = match CStr::from_ptr(request_path).to_str() {
271 Ok(s) => s,
272 Err(e) => {
273 set_last_error(format!("invalid UTF-8 in request_path: {e}"));
274 return std::ptr::null_mut();
275 }
276 };
277
278 let protocol = match WebUIProtocol::from_protobuf(protocol_bytes) {
280 Ok(p) => p,
281 Err(e) => {
282 set_last_error(format!("failed to parse protobuf protocol: {e}"));
283 return std::ptr::null_mut();
284 }
285 };
286
287 let data: Value = match serde_json::from_str(data_str) {
289 Ok(d) => d,
290 Err(e) => {
291 set_last_error(format!("failed to parse data JSON: {e}"));
292 return std::ptr::null_mut();
293 }
294 };
295
296 let mut writer = StringResponseWriter::new();
298 match context.handler.render(
299 &protocol,
300 &data,
301 &RenderOptions::new(entry_str, path_str),
302 &mut writer,
303 ) {
304 Ok(_) => match CString::new(writer.content) {
305 Ok(s) => s.into_raw(),
306 Err(e) => {
307 set_last_error(format!("rendered output contains interior NUL byte: {e}"));
308 std::ptr::null_mut()
309 }
310 },
311 Err(e) => {
312 set_last_error(format!("render failed: {e}"));
313 std::ptr::null_mut()
314 }
315 }
316}
317
318#[no_mangle]
346pub unsafe extern "C" fn webui_render(
347 html: *const c_char,
348 data_json: *const c_char,
349) -> *mut c_char {
350 clear_last_error();
351
352 if html.is_null() || data_json.is_null() {
353 set_last_error("html and data_json must not be null");
354 return std::ptr::null_mut();
355 }
356
357 #[cfg(not(feature = "parser"))]
358 {
359 let _ = (html, data_json);
360 set_last_error(
361 "webui_render requires the \"parser\" feature, which was not enabled at build time",
362 );
363 return std::ptr::null_mut();
364 }
365
366 #[cfg(feature = "parser")]
367 match std::panic::catch_unwind(|| webui_render_impl(html, data_json)) {
368 Ok(ptr) => ptr,
369 Err(_) => {
370 set_last_error("panic in webui_render");
371 std::ptr::null_mut()
372 }
373 }
374}
375
376#[cfg(feature = "parser")]
378unsafe fn webui_render_impl(html: *const c_char, data_json: *const c_char) -> *mut c_char {
379 let html_str = match CStr::from_ptr(html).to_str() {
382 Ok(s) => s,
383 Err(e) => {
384 set_last_error(format!("invalid UTF-8 in html: {e}"));
385 return std::ptr::null_mut();
386 }
387 };
388
389 let data_str = match CStr::from_ptr(data_json).to_str() {
391 Ok(s) => s,
392 Err(e) => {
393 set_last_error(format!("invalid UTF-8 in data_json: {e}"));
394 return std::ptr::null_mut();
395 }
396 };
397
398 let entry_key = "template";
400 let mut parser = HtmlParser::new();
401 if let Err(e) = parser.parse(entry_key, html_str) {
402 set_last_error(format!("HTML parse error: {e}"));
403 return std::ptr::null_mut();
404 }
405
406 let protocol = WebUIProtocol::new(parser.into_fragment_records());
407
408 let data: Value = match serde_json::from_str(data_str) {
410 Ok(d) => d,
411 Err(e) => {
412 set_last_error(format!("failed to parse data JSON: {e}"));
413 return std::ptr::null_mut();
414 }
415 };
416
417 let handler = WebUIHandler::new();
419 let mut writer = StringResponseWriter::new();
420
421 match handler.render(
422 &protocol,
423 &data,
424 &RenderOptions::new(entry_key, "/"),
425 &mut writer,
426 ) {
427 Ok(_) => match CString::new(writer.content) {
428 Ok(s) => s.into_raw(),
429 Err(e) => {
430 set_last_error(format!("rendered output contains interior NUL byte: {e}"));
431 std::ptr::null_mut()
432 }
433 },
434 Err(e) => {
435 set_last_error(format!("render failed: {e}"));
436 std::ptr::null_mut()
437 }
438 }
439}
440
441#[no_mangle]
469pub unsafe extern "C" fn webui_render_partial(
470 protocol_data: *const u8,
471 protocol_len: usize,
472 state_json: *const c_char,
473 entry_id: *const c_char,
474 request_path: *const c_char,
475 inventory_hex: *const c_char,
476) -> *mut c_char {
477 clear_last_error();
478
479 match std::panic::catch_unwind(|| {
480 if protocol_data.is_null()
481 || state_json.is_null()
482 || entry_id.is_null()
483 || request_path.is_null()
484 || inventory_hex.is_null()
485 {
486 set_last_error("one or more required arguments are null");
487 return std::ptr::null_mut();
488 }
489
490 let protocol_bytes = unsafe { std::slice::from_raw_parts(protocol_data, protocol_len) };
492
493 let state_str = match unsafe { CStr::from_ptr(state_json) }.to_str() {
495 Ok(s) => s,
496 Err(e) => {
497 set_last_error(format!("invalid UTF-8 in state_json: {e}"));
498 return std::ptr::null_mut();
499 }
500 };
501
502 let state: Value = match serde_json::from_str(state_str) {
503 Ok(v) => v,
504 Err(e) => {
505 set_last_error(format!("invalid state JSON: {e}"));
506 return std::ptr::null_mut();
507 }
508 };
509
510 let entry_str = match unsafe { CStr::from_ptr(entry_id) }.to_str() {
512 Ok(s) => s,
513 Err(e) => {
514 set_last_error(format!("invalid UTF-8 in entry_id: {e}"));
515 return std::ptr::null_mut();
516 }
517 };
518
519 let request_path_str = match unsafe { CStr::from_ptr(request_path) }.to_str() {
521 Ok(s) => s,
522 Err(e) => {
523 set_last_error(format!("invalid UTF-8 in request_path: {e}"));
524 return std::ptr::null_mut();
525 }
526 };
527
528 let inv_str = match unsafe { CStr::from_ptr(inventory_hex) }.to_str() {
530 Ok(s) => s,
531 Err(e) => {
532 set_last_error(format!("invalid UTF-8 in inventory_hex: {e}"));
533 return std::ptr::null_mut();
534 }
535 };
536
537 let protocol = match WebUIProtocol::from_protobuf(protocol_bytes) {
538 Ok(p) => p,
539 Err(e) => {
540 set_last_error(format!("failed to parse protobuf protocol: {e}"));
541 return std::ptr::null_mut();
542 }
543 };
544
545 let mut index = webui_handler::route_handler::ProtocolIndex::new(&protocol);
547
548 let mut result = match webui_handler::route_handler::render_partial(
549 &protocol,
550 entry_str,
551 request_path_str,
552 inv_str,
553 &mut index,
554 ) {
555 Ok(v) => v,
556 Err(e) => {
557 set_last_error(format!("render_partial failed: {e}"));
558 return std::ptr::null_mut();
559 }
560 };
561 if let Some(obj) = result.as_object_mut() {
562 obj.insert("state".into(), state);
563 }
564
565 match CString::new(result.to_string()) {
566 Ok(s) => s.into_raw(),
567 Err(e) => {
568 set_last_error(format!("JSON output contains interior NUL byte: {e}"));
569 std::ptr::null_mut()
570 }
571 }
572 }) {
573 Ok(ptr) => ptr,
574 Err(_) => {
575 set_last_error("panic in webui_render_partial");
576 std::ptr::null_mut()
577 }
578 }
579}
580
581#[no_mangle]
592pub unsafe extern "C" fn webui_render_component_templates(
593 protocol_data: *const u8,
594 protocol_len: usize,
595 component_tags_json: *const c_char,
596 inventory_hex: *const c_char,
597) -> *mut c_char {
598 clear_last_error();
599
600 match std::panic::catch_unwind(|| {
601 if protocol_data.is_null() || component_tags_json.is_null() || inventory_hex.is_null() {
602 set_last_error("one or more required arguments are null");
603 return std::ptr::null_mut();
604 }
605
606 let protocol_bytes = unsafe { std::slice::from_raw_parts(protocol_data, protocol_len) };
607
608 let tags_str = match unsafe { CStr::from_ptr(component_tags_json) }.to_str() {
609 Ok(s) => s,
610 Err(e) => {
611 set_last_error(format!("invalid UTF-8 in component_tags_json: {e}"));
612 return std::ptr::null_mut();
613 }
614 };
615
616 let tags: Vec<String> = match serde_json::from_str(tags_str) {
617 Ok(v) => v,
618 Err(e) => {
619 set_last_error(format!("invalid tags JSON: {e}"));
620 return std::ptr::null_mut();
621 }
622 };
623 let tag_refs: Vec<&str> = tags.iter().map(|s| s.as_str()).collect();
624
625 let inv_str = match unsafe { CStr::from_ptr(inventory_hex) }.to_str() {
626 Ok(s) => s,
627 Err(e) => {
628 set_last_error(format!("invalid UTF-8 in inventory_hex: {e}"));
629 return std::ptr::null_mut();
630 }
631 };
632
633 let protocol = match WebUIProtocol::from_protobuf(protocol_bytes) {
634 Ok(p) => p,
635 Err(e) => {
636 set_last_error(format!("failed to parse protobuf protocol: {e}"));
637 return std::ptr::null_mut();
638 }
639 };
640
641 let index = webui_handler::route_handler::ProtocolIndex::new(&protocol);
643
644 let result = match webui_handler::route_handler::render_component_templates(
645 &protocol, &tag_refs, inv_str, &index,
646 ) {
647 Ok(v) => v,
648 Err(e) => {
649 set_last_error(format!("render_component_templates failed: {e}"));
650 return std::ptr::null_mut();
651 }
652 };
653
654 match CString::new(result.to_string()) {
655 Ok(s) => s.into_raw(),
656 Err(e) => {
657 set_last_error(format!("JSON output contains interior NUL byte: {e}"));
658 std::ptr::null_mut()
659 }
660 }
661 }) {
662 Ok(ptr) => ptr,
663 Err(_) => {
664 set_last_error("panic in webui_render_component_templates");
665 std::ptr::null_mut()
666 }
667 }
668}
669
670#[no_mangle]
678pub unsafe extern "C" fn webui_free(string_ptr: *mut c_char) {
679 if !string_ptr.is_null() {
680 let _ = CString::from_raw(string_ptr);
681 }
682}
683
684#[no_mangle]
699pub unsafe extern "C" fn webui_protocol_tokens(
700 protocol_data: *const u8,
701 protocol_len: usize,
702) -> *mut c_char {
703 clear_last_error();
704
705 match std::panic::catch_unwind(|| {
706 if protocol_data.is_null() {
707 set_last_error("protocol_data is null");
708 return std::ptr::null_mut();
709 }
710
711 let proto_bytes = unsafe { std::slice::from_raw_parts(protocol_data, protocol_len) };
713
714 let protocol = match WebUIProtocol::from_protobuf(proto_bytes) {
715 Ok(p) => p,
716 Err(e) => {
717 set_last_error(format!("failed to parse protobuf: {e}"));
718 return std::ptr::null_mut();
719 }
720 };
721
722 let joined = protocol.tokens.join("\n");
724
725 match CString::new(joined) {
726 Ok(cs) => cs.into_raw(),
727 Err(e) => {
728 set_last_error(format!("token string contains null byte: {e}"));
729 std::ptr::null_mut()
730 }
731 }
732 }) {
733 Ok(ptr) => ptr,
734 Err(_) => {
735 set_last_error("panic in webui_protocol_tokens");
736 std::ptr::null_mut()
737 }
738 }
739}