#![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::{Protocol, RenderOptions, ResponseWriter, WebUIHandler};
#[allow(non_camel_case_types)]
pub type webui_protocol_t = c_void;
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 ProtocolContext {
protocol: Protocol,
}
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_protocol_create(
protocol_data: *const u8,
protocol_len: usize,
) -> *mut webui_protocol_t {
clear_last_error();
if protocol_data.is_null() {
set_last_error("protocol_data is null");
return std::ptr::null_mut();
}
match std::panic::catch_unwind(|| {
let bytes = unsafe { std::slice::from_raw_parts(protocol_data, protocol_len) };
match Protocol::from_protobuf(bytes) {
Ok(protocol) => {
Box::into_raw(Box::new(ProtocolContext { protocol })) as *mut webui_protocol_t
}
Err(error) => {
set_last_error(format!("failed to parse protobuf protocol: {error}"));
std::ptr::null_mut()
}
}
}) {
Ok(ptr) => ptr,
Err(_) => {
set_last_error("panic in webui_protocol_create");
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn webui_protocol_destroy(protocol_ptr: *mut webui_protocol_t) {
if !protocol_ptr.is_null() {
let _ = unsafe { Box::from_raw(protocol_ptr as *mut ProtocolContext) };
}
}
#[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_ptr: *const webui_protocol_t,
data_json: *const c_char,
entry_id: *const c_char,
request_path: *const c_char,
) -> *mut c_char {
clear_last_error();
match std::panic::catch_unwind(|| {
if handler_ptr.is_null()
|| protocol_ptr.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 = unsafe { &*(handler_ptr as *const HandlerContext) };
let protocol_context = unsafe { &*(protocol_ptr as *const ProtocolContext) };
unsafe {
render_decoded_protocol(
context,
&protocol_context.protocol,
data_json,
entry_id,
request_path,
)
}
}) {
Ok(ptr) => ptr,
Err(_) => {
set_last_error("panic in webui_handler_render");
std::ptr::null_mut()
}
}
}
unsafe fn render_decoded_protocol(
context: &HandlerContext,
protocol: &Protocol,
data_json: *const c_char,
entry_id: *const c_char,
request_path: *const c_char,
) -> *mut c_char {
let data_str = match unsafe { 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 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 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 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_protocol_render_partial(
protocol_ptr: *const webui_protocol_t,
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_ptr.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_context = unsafe { &*(protocol_ptr as *const ProtocolContext) };
let state_str = match unsafe { CStr::from_ptr(state_json) }.to_str() {
Ok(value) => value,
Err(error) => {
set_last_error(format!("invalid UTF-8 in state_json: {error}"));
return std::ptr::null_mut();
}
};
let entry_str = match unsafe { CStr::from_ptr(entry_id) }.to_str() {
Ok(value) => value,
Err(error) => {
set_last_error(format!("invalid UTF-8 in entry_id: {error}"));
return std::ptr::null_mut();
}
};
let request_path_str = match unsafe { CStr::from_ptr(request_path) }.to_str() {
Ok(value) => value,
Err(error) => {
set_last_error(format!("invalid UTF-8 in request_path: {error}"));
return std::ptr::null_mut();
}
};
let inventory_str = match unsafe { CStr::from_ptr(inventory_hex) }.to_str() {
Ok(value) => value,
Err(error) => {
set_last_error(format!("invalid UTF-8 in inventory_hex: {error}"));
return std::ptr::null_mut();
}
};
let output = match protocol_context.protocol.render_partial(
state_str,
entry_str,
request_path_str,
inventory_str,
) {
Ok(value) => value,
Err(error) => {
set_last_error(format!("render_partial failed: {error}"));
return std::ptr::null_mut();
}
};
match CString::new(output) {
Ok(value) => value.into_raw(),
Err(error) => {
set_last_error(format!("JSON output contains interior NUL byte: {error}"));
std::ptr::null_mut()
}
}
}) {
Ok(ptr) => ptr,
Err(_) => {
set_last_error("panic in webui_protocol_render_partial");
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn webui_protocol_render_component_templates(
protocol_ptr: *const webui_protocol_t,
component_tags_json: *const c_char,
inventory_hex: *const c_char,
) -> *mut c_char {
clear_last_error();
match std::panic::catch_unwind(|| {
if protocol_ptr.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_context = unsafe { &*(protocol_ptr as *const ProtocolContext) };
let tags_str = match unsafe { CStr::from_ptr(component_tags_json) }.to_str() {
Ok(value) => value,
Err(error) => {
set_last_error(format!("invalid UTF-8 in component_tags_json: {error}"));
return std::ptr::null_mut();
}
};
let tags: Vec<String> = match serde_json::from_str(tags_str) {
Ok(value) => value,
Err(error) => {
set_last_error(format!("invalid tags JSON: {error}"));
return std::ptr::null_mut();
}
};
let tag_refs: Vec<&str> = tags.iter().map(String::as_str).collect();
let inventory_str = match unsafe { CStr::from_ptr(inventory_hex) }.to_str() {
Ok(value) => value,
Err(error) => {
set_last_error(format!("invalid UTF-8 in inventory_hex: {error}"));
return std::ptr::null_mut();
}
};
let result = match protocol_context
.protocol
.render_component_templates(&tag_refs, inventory_str)
{
Ok(value) => value,
Err(error) => {
set_last_error(format!("render_component_templates failed: {error}"));
return std::ptr::null_mut();
}
};
match CString::new(result.to_string()) {
Ok(value) => value.into_raw(),
Err(error) => {
set_last_error(format!("JSON output contains interior NUL byte: {error}"));
std::ptr::null_mut()
}
}
}) {
Ok(ptr) => ptr,
Err(_) => {
set_last_error("panic in webui_protocol_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_ptr: *const webui_protocol_t,
) -> *mut c_char {
clear_last_error();
match std::panic::catch_unwind(|| {
if protocol_ptr.is_null() {
set_last_error("protocol_ptr is null");
return std::ptr::null_mut();
}
let protocol_context = unsafe { &*(protocol_ptr as *const ProtocolContext) };
let joined = protocol_context.protocol.tokens().join("\n");
match CString::new(joined) {
Ok(value) => value.into_raw(),
Err(error) => {
set_last_error(format!("token string contains null byte: {error}"));
std::ptr::null_mut()
}
}
}) {
Ok(ptr) => ptr,
Err(_) => {
set_last_error("panic in webui_protocol_tokens");
std::ptr::null_mut()
}
}
}