#![allow(unsafe_code)]
use serde_json::Value;
use std::cell::RefCell;
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_void};
use webui_handler::plugin::fast_v2::FastV2HydrationPlugin;
use webui_handler::plugin::fast_v3::FastV3HydrationPlugin;
use webui_handler::plugin::webui::WebUIHydrationPlugin;
use webui_handler::{RenderOptions, ResponseWriter, WebUIHandler};
#[cfg(feature = "parser")]
use webui_parser::HtmlParser;
use webui_protocol::WebUIProtocol;
thread_local! {
static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
}
fn set_last_error(msg: impl Into<String>) {
let mut bytes = msg.into().into_bytes();
if let Some(nul_pos) = bytes.iter().position(|byte| *byte == 0) {
bytes.truncate(nul_pos);
}
let c_string = unsafe { CString::from_vec_unchecked(bytes) };
LAST_ERROR.with(|cell| {
cell.replace(Some(c_string));
});
}
fn clear_last_error() {
LAST_ERROR.with(|cell| {
cell.replace(None);
});
}
struct HandlerContext {
handler: WebUIHandler,
nonce: Option<String>,
}
struct StringResponseWriter {
content: String,
}
impl StringResponseWriter {
fn new() -> Self {
Self {
content: String::new(),
}
}
}
impl ResponseWriter for StringResponseWriter {
fn write(&mut self, content: &str) -> webui_handler::Result<()> {
self.content.push_str(content);
Ok(())
}
fn end(&mut self) -> webui_handler::Result<()> {
Ok(())
}
}
#[no_mangle]
pub extern "C" fn webui_last_error() -> *const c_char {
LAST_ERROR.with(|cell| {
let borrow = cell.borrow();
match borrow.as_ref() {
Some(c_str) => c_str.as_ptr(),
None => std::ptr::null(),
}
})
}
#[no_mangle]
pub extern "C" fn webui_handler_create() -> *mut c_void {
let handler = WebUIHandler::new();
let context = Box::new(HandlerContext {
handler,
nonce: None,
});
Box::into_raw(context) as *mut c_void
}
#[no_mangle]
pub unsafe extern "C" fn webui_handler_create_with_plugin(plugin_id: *const c_char) -> *mut c_void {
clear_last_error();
let handler = if plugin_id.is_null() {
WebUIHandler::new()
} else {
match CStr::from_ptr(plugin_id).to_str() {
Ok("fast" | "fast-v2") => {
WebUIHandler::with_plugin(|| Box::new(FastV2HydrationPlugin::new()))
}
Ok("fast-v3") => WebUIHandler::with_plugin(|| Box::new(FastV3HydrationPlugin::new())),
Ok("webui") => WebUIHandler::with_plugin(|| Box::new(WebUIHydrationPlugin::new())),
Ok(unknown) => {
set_last_error(format!(
"unknown plugin: {unknown}. Use \"webui\", \"fast-v3\", \"fast-v2\", or \"fast\"."
));
return std::ptr::null_mut();
}
Err(e) => {
set_last_error(format!("invalid UTF-8 in plugin_id: {e}"));
return std::ptr::null_mut();
}
}
};
let context = Box::new(HandlerContext {
handler,
nonce: None,
});
Box::into_raw(context) as *mut c_void
}
#[no_mangle]
pub unsafe extern "C" fn webui_handler_destroy(handler_ptr: *mut c_void) {
if !handler_ptr.is_null() {
let _ = Box::from_raw(handler_ptr as *mut HandlerContext);
}
}
#[no_mangle]
pub unsafe extern "C" fn webui_handler_set_nonce(handler_ptr: *mut c_void, nonce: *const c_char) {
clear_last_error();
if handler_ptr.is_null() {
set_last_error("handler_ptr is null");
return;
}
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let context = &mut *(handler_ptr as *mut HandlerContext);
if nonce.is_null() {
context.nonce = None;
} else {
match CStr::from_ptr(nonce).to_str() {
Ok(s) => context.nonce = Some(s.to_string()),
Err(e) => {
set_last_error(format!("invalid UTF-8 in nonce: {e}"));
}
}
}
})) {
Ok(()) => {}
Err(_) => {
set_last_error("panic in webui_handler_set_nonce");
}
}
}
#[no_mangle]
pub unsafe extern "C" fn webui_handler_render(
handler_ptr: *mut c_void,
protocol_data: *const u8,
protocol_len: usize,
data_json: *const c_char,
entry_id: *const c_char,
request_path: *const c_char,
) -> *mut c_char {
clear_last_error();
if handler_ptr.is_null()
|| protocol_data.is_null()
|| data_json.is_null()
|| entry_id.is_null()
|| request_path.is_null()
{
set_last_error("one or more required arguments are null");
return std::ptr::null_mut();
}
let context = &*(handler_ptr as *const HandlerContext);
let protocol_bytes = std::slice::from_raw_parts(protocol_data, protocol_len);
let data_str = match CStr::from_ptr(data_json).to_str() {
Ok(s) => s,
Err(e) => {
set_last_error(format!("invalid UTF-8 in data_json: {e}"));
return std::ptr::null_mut();
}
};
let entry_str = match CStr::from_ptr(entry_id).to_str() {
Ok(s) => s,
Err(e) => {
set_last_error(format!("invalid UTF-8 in entry_id: {e}"));
return std::ptr::null_mut();
}
};
let path_str = match CStr::from_ptr(request_path).to_str() {
Ok(s) => s,
Err(e) => {
set_last_error(format!("invalid UTF-8 in request_path: {e}"));
return std::ptr::null_mut();
}
};
let protocol = match WebUIProtocol::from_protobuf(protocol_bytes) {
Ok(p) => p,
Err(e) => {
set_last_error(format!("failed to parse protobuf protocol: {e}"));
return std::ptr::null_mut();
}
};
let data: Value = match serde_json::from_str(data_str) {
Ok(d) => d,
Err(e) => {
set_last_error(format!("failed to parse data JSON: {e}"));
return std::ptr::null_mut();
}
};
let mut options = RenderOptions::new(entry_str, path_str);
if let Some(ref nonce) = context.nonce {
options = options.with_nonce(nonce);
}
let mut writer = StringResponseWriter::new();
match context
.handler
.render(&protocol, &data, &options, &mut writer)
{
Ok(_) => match CString::new(writer.content) {
Ok(s) => s.into_raw(),
Err(e) => {
set_last_error(format!("rendered output contains interior NUL byte: {e}"));
std::ptr::null_mut()
}
},
Err(e) => {
set_last_error(format!("render failed: {e}"));
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn webui_render(
html: *const c_char,
data_json: *const c_char,
) -> *mut c_char {
clear_last_error();
if html.is_null() || data_json.is_null() {
set_last_error("html and data_json must not be null");
return std::ptr::null_mut();
}
#[cfg(not(feature = "parser"))]
{
let _ = (html, data_json);
set_last_error(
"webui_render requires the \"parser\" feature, which was not enabled at build time",
);
return std::ptr::null_mut();
}
#[cfg(feature = "parser")]
match std::panic::catch_unwind(|| webui_render_impl(html, data_json)) {
Ok(ptr) => ptr,
Err(_) => {
set_last_error("panic in webui_render");
std::ptr::null_mut()
}
}
}
#[cfg(feature = "parser")]
unsafe fn webui_render_impl(html: *const c_char, data_json: *const c_char) -> *mut c_char {
let html_str = match CStr::from_ptr(html).to_str() {
Ok(s) => s,
Err(e) => {
set_last_error(format!("invalid UTF-8 in html: {e}"));
return std::ptr::null_mut();
}
};
let data_str = match CStr::from_ptr(data_json).to_str() {
Ok(s) => s,
Err(e) => {
set_last_error(format!("invalid UTF-8 in data_json: {e}"));
return std::ptr::null_mut();
}
};
let entry_key = "template";
let mut parser = HtmlParser::new();
if let Err(e) = parser.parse(entry_key, html_str) {
set_last_error(format!("HTML parse error: {e}"));
return std::ptr::null_mut();
}
let protocol = WebUIProtocol::new(parser.into_fragment_records());
let data: Value = match serde_json::from_str(data_str) {
Ok(d) => d,
Err(e) => {
set_last_error(format!("failed to parse data JSON: {e}"));
return std::ptr::null_mut();
}
};
let handler = WebUIHandler::new();
let mut writer = StringResponseWriter::new();
match handler.render(
&protocol,
&data,
&RenderOptions::new(entry_key, "/"),
&mut writer,
) {
Ok(_) => match CString::new(writer.content) {
Ok(s) => s.into_raw(),
Err(e) => {
set_last_error(format!("rendered output contains interior NUL byte: {e}"));
std::ptr::null_mut()
}
},
Err(e) => {
set_last_error(format!("render failed: {e}"));
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn webui_render_partial(
protocol_data: *const u8,
protocol_len: usize,
state_json: *const c_char,
entry_id: *const c_char,
request_path: *const c_char,
inventory_hex: *const c_char,
) -> *mut c_char {
clear_last_error();
match std::panic::catch_unwind(|| {
if protocol_data.is_null()
|| state_json.is_null()
|| entry_id.is_null()
|| request_path.is_null()
|| inventory_hex.is_null()
{
set_last_error("one or more required arguments are null");
return std::ptr::null_mut();
}
let protocol_bytes = unsafe { std::slice::from_raw_parts(protocol_data, protocol_len) };
let state_str = match unsafe { CStr::from_ptr(state_json) }.to_str() {
Ok(s) => s,
Err(e) => {
set_last_error(format!("invalid UTF-8 in state_json: {e}"));
return std::ptr::null_mut();
}
};
let state: Value = match serde_json::from_str(state_str) {
Ok(v) => v,
Err(e) => {
set_last_error(format!("invalid state JSON: {e}"));
return std::ptr::null_mut();
}
};
let entry_str = match unsafe { CStr::from_ptr(entry_id) }.to_str() {
Ok(s) => s,
Err(e) => {
set_last_error(format!("invalid UTF-8 in entry_id: {e}"));
return std::ptr::null_mut();
}
};
let request_path_str = match unsafe { CStr::from_ptr(request_path) }.to_str() {
Ok(s) => s,
Err(e) => {
set_last_error(format!("invalid UTF-8 in request_path: {e}"));
return std::ptr::null_mut();
}
};
let inv_str = match unsafe { CStr::from_ptr(inventory_hex) }.to_str() {
Ok(s) => s,
Err(e) => {
set_last_error(format!("invalid UTF-8 in inventory_hex: {e}"));
return std::ptr::null_mut();
}
};
let protocol = match WebUIProtocol::from_protobuf(protocol_bytes) {
Ok(p) => p,
Err(e) => {
set_last_error(format!("failed to parse protobuf protocol: {e}"));
return std::ptr::null_mut();
}
};
let mut index = webui_handler::route_handler::ProtocolIndex::new(&protocol);
let mut result = match webui_handler::route_handler::render_partial(
&protocol,
entry_str,
request_path_str,
inv_str,
&mut index,
) {
Ok(v) => v,
Err(e) => {
set_last_error(format!("render_partial failed: {e}"));
return std::ptr::null_mut();
}
};
if let Some(obj) = result.as_object_mut() {
obj.insert("state".into(), state);
}
match CString::new(result.to_string()) {
Ok(s) => s.into_raw(),
Err(e) => {
set_last_error(format!("JSON output contains interior NUL byte: {e}"));
std::ptr::null_mut()
}
}
}) {
Ok(ptr) => ptr,
Err(_) => {
set_last_error("panic in webui_render_partial");
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn webui_render_component_templates(
protocol_data: *const u8,
protocol_len: usize,
component_tags_json: *const c_char,
inventory_hex: *const c_char,
) -> *mut c_char {
clear_last_error();
match std::panic::catch_unwind(|| {
if protocol_data.is_null() || component_tags_json.is_null() || inventory_hex.is_null() {
set_last_error("one or more required arguments are null");
return std::ptr::null_mut();
}
let protocol_bytes = unsafe { std::slice::from_raw_parts(protocol_data, protocol_len) };
let tags_str = match unsafe { CStr::from_ptr(component_tags_json) }.to_str() {
Ok(s) => s,
Err(e) => {
set_last_error(format!("invalid UTF-8 in component_tags_json: {e}"));
return std::ptr::null_mut();
}
};
let tags: Vec<String> = match serde_json::from_str(tags_str) {
Ok(v) => v,
Err(e) => {
set_last_error(format!("invalid tags JSON: {e}"));
return std::ptr::null_mut();
}
};
let tag_refs: Vec<&str> = tags.iter().map(|s| s.as_str()).collect();
let inv_str = match unsafe { CStr::from_ptr(inventory_hex) }.to_str() {
Ok(s) => s,
Err(e) => {
set_last_error(format!("invalid UTF-8 in inventory_hex: {e}"));
return std::ptr::null_mut();
}
};
let protocol = match WebUIProtocol::from_protobuf(protocol_bytes) {
Ok(p) => p,
Err(e) => {
set_last_error(format!("failed to parse protobuf protocol: {e}"));
return std::ptr::null_mut();
}
};
let index = webui_handler::route_handler::ProtocolIndex::new(&protocol);
let result = match webui_handler::route_handler::render_component_templates(
&protocol, &tag_refs, inv_str, &index,
) {
Ok(v) => v,
Err(e) => {
set_last_error(format!("render_component_templates failed: {e}"));
return std::ptr::null_mut();
}
};
match CString::new(result.to_string()) {
Ok(s) => s.into_raw(),
Err(e) => {
set_last_error(format!("JSON output contains interior NUL byte: {e}"));
std::ptr::null_mut()
}
}
}) {
Ok(ptr) => ptr,
Err(_) => {
set_last_error("panic in webui_render_component_templates");
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn webui_free(string_ptr: *mut c_char) {
if !string_ptr.is_null() {
let _ = CString::from_raw(string_ptr);
}
}
#[no_mangle]
pub unsafe extern "C" fn webui_protocol_tokens(
protocol_data: *const u8,
protocol_len: usize,
) -> *mut c_char {
clear_last_error();
match std::panic::catch_unwind(|| {
if protocol_data.is_null() {
set_last_error("protocol_data is null");
return std::ptr::null_mut();
}
let proto_bytes = unsafe { std::slice::from_raw_parts(protocol_data, protocol_len) };
let protocol = match WebUIProtocol::from_protobuf(proto_bytes) {
Ok(p) => p,
Err(e) => {
set_last_error(format!("failed to parse protobuf: {e}"));
return std::ptr::null_mut();
}
};
let joined = protocol.tokens.join("\n");
match CString::new(joined) {
Ok(cs) => cs.into_raw(),
Err(e) => {
set_last_error(format!("token string contains null byte: {e}"));
std::ptr::null_mut()
}
}
}) {
Ok(ptr) => ptr,
Err(_) => {
set_last_error("panic in webui_protocol_tokens");
std::ptr::null_mut()
}
}
}