#![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 std::sync::Arc;
use webui_handler::plugin::fast_v2::FastV2HydrationPlugin;
use webui_handler::plugin::fast_v3::FastV3HydrationPlugin;
use webui_handler::plugin::webui::WebUIHydrationPlugin;
use webui_handler::{
BoundaryId, BoundaryMode, Protocol, RenderOptions, ResponseWriter, SessionOptions,
StreamingSession, 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: Arc<WebUIHandler>,
nonce: Option<String>,
}
struct ProtocolContext {
protocol: Arc<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: Arc::new(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: Arc::new(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: Arc::new(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()
}
}
}
#[allow(non_camel_case_types)]
pub type webui_streaming_session_t = c_void;
struct StreamingSessionContext {
session: StreamingSession,
}
#[no_mangle]
pub unsafe extern "C" fn webui_streaming_session_create(
handler_ptr: *mut c_void,
protocol_ptr: *const webui_protocol_t,
entry_id: *const c_char,
request_path: *const c_char,
) -> *mut webui_streaming_session_t {
clear_last_error();
match std::panic::catch_unwind(|| {
if handler_ptr.is_null()
|| protocol_ptr.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 handler_context = unsafe { &*(handler_ptr as *const HandlerContext) };
let protocol_context = unsafe { &*(protocol_ptr as *const ProtocolContext) };
let Some(entry) = (unsafe { utf8_arg(entry_id, "entry_id") }) else {
return std::ptr::null_mut();
};
let Some(path) = (unsafe { utf8_arg(request_path, "request_path") }) else {
return std::ptr::null_mut();
};
let mut options = SessionOptions::new(entry, path);
options.nonce = handler_context.nonce.clone();
match StreamingSession::new(
Arc::clone(&handler_context.handler),
Arc::clone(&protocol_context.protocol),
options,
) {
Ok(session) => Box::into_raw(Box::new(StreamingSessionContext { session }))
as *mut webui_streaming_session_t,
Err(error) => {
set_last_error(error.to_string());
std::ptr::null_mut()
}
}
}) {
Ok(ptr) => ptr,
Err(_) => {
set_last_error("panic in webui_streaming_session_create");
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn webui_streaming_session_destroy(
session_ptr: *mut webui_streaming_session_t,
) {
if !session_ptr.is_null() {
let _ = unsafe { Box::from_raw(session_ptr as *mut StreamingSessionContext) };
}
}
#[no_mangle]
pub unsafe extern "C" fn webui_streaming_session_boundary(
session_ptr: *const webui_streaming_session_t,
name: *const c_char,
out_boundary: *mut u32,
) -> bool {
clear_last_error();
match std::panic::catch_unwind(|| {
if session_ptr.is_null() || name.is_null() || out_boundary.is_null() {
set_last_error("one or more required arguments are null");
return false;
}
let context = unsafe { &*(session_ptr as *const StreamingSessionContext) };
let Some(name) = (unsafe { utf8_arg(name, "name") }) else {
return false;
};
match context.session.boundary(name) {
Ok(boundary) => {
unsafe { *out_boundary = boundary.raw() };
true
}
Err(error) => {
set_last_error(error.to_string());
false
}
}
}) {
Ok(ok) => ok,
Err(_) => {
set_last_error("panic in webui_streaming_session_boundary");
false
}
}
}
#[no_mangle]
pub unsafe extern "C" fn webui_streaming_session_boundary_count(
session_ptr: *const webui_streaming_session_t,
) -> u32 {
if session_ptr.is_null() {
return 0;
}
let context = unsafe { &*(session_ptr as *const StreamingSessionContext) };
u32::try_from(context.session.boundary_count()).unwrap_or(u32::MAX)
}
#[no_mangle]
pub unsafe extern "C" fn webui_streaming_session_is_finished(
session_ptr: *const webui_streaming_session_t,
) -> bool {
if session_ptr.is_null() {
return true;
}
let context = unsafe { &*(session_ptr as *const StreamingSessionContext) };
context.session.is_finished()
}
#[no_mangle]
pub unsafe extern "C" fn webui_streaming_session_write_shell(
session_ptr: *mut webui_streaming_session_t,
state_json: *const c_char,
out_len: *mut usize,
) -> *mut c_char {
unsafe {
streaming_chunk_call(
session_ptr,
state_json,
out_len,
"webui_streaming_session_write_shell",
|session, state| session.write_shell(state),
)
}
}
#[no_mangle]
pub unsafe extern "C" fn webui_streaming_session_write_boundary(
session_ptr: *mut webui_streaming_session_t,
boundary: u32,
state_json: *const c_char,
updatable: bool,
out_len: *mut usize,
) -> *mut c_char {
let mode = if updatable {
BoundaryMode::Updatable
} else {
BoundaryMode::Final
};
unsafe {
streaming_chunk_call(
session_ptr,
state_json,
out_len,
"webui_streaming_session_write_boundary",
|session, state| session.write_boundary(BoundaryId::from_raw(boundary), state, mode),
)
}
}
#[no_mangle]
pub unsafe extern "C" fn webui_streaming_session_update(
session_ptr: *mut webui_streaming_session_t,
boundary: u32,
state_json: *const c_char,
out_len: *mut usize,
) -> *mut c_char {
unsafe {
streaming_chunk_call(
session_ptr,
state_json,
out_len,
"webui_streaming_session_update",
|session, state| session.update(BoundaryId::from_raw(boundary), state),
)
}
}
#[no_mangle]
pub unsafe extern "C" fn webui_streaming_session_finish(
session_ptr: *mut webui_streaming_session_t,
state_json: *const c_char,
out_len: *mut usize,
) -> *mut c_char {
unsafe {
streaming_chunk_call(
session_ptr,
state_json,
out_len,
"webui_streaming_session_finish",
|session, state| session.finish(state),
)
}
}
unsafe fn streaming_chunk_call(
session_ptr: *mut webui_streaming_session_t,
state_json: *const c_char,
out_len: *mut usize,
operation: &str,
render: impl FnOnce(&mut StreamingSession, &Value) -> webui_handler::Result<Vec<u8>>,
) -> *mut c_char {
clear_last_error();
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if session_ptr.is_null() || state_json.is_null() {
set_last_error("one or more required arguments are null");
return std::ptr::null_mut();
}
let context = unsafe { &mut *(session_ptr as *mut StreamingSessionContext) };
let Some(state_str) = (unsafe { utf8_arg(state_json, "state_json") }) else {
return std::ptr::null_mut();
};
let state: Value = match serde_json::from_str(state_str) {
Ok(value) => value,
Err(error) => {
set_last_error(format!("failed to parse state JSON: {error}"));
return std::ptr::null_mut();
}
};
let bytes = match render(&mut context.session, &state) {
Ok(bytes) => bytes,
Err(error) => {
set_last_error(error.to_string());
return std::ptr::null_mut();
}
};
let length = bytes.len();
match CString::new(bytes) {
Ok(chunk) => {
if !out_len.is_null() {
unsafe { *out_len = length };
}
chunk.into_raw()
}
Err(error) => {
set_last_error(format!("chunk contains interior NUL byte: {error}"));
std::ptr::null_mut()
}
}
})) {
Ok(ptr) => ptr,
Err(_) => {
set_last_error(format!("panic in {operation}"));
std::ptr::null_mut()
}
}
}
unsafe fn utf8_arg<'a>(value: *const c_char, name: &str) -> Option<&'a str> {
match unsafe { CStr::from_ptr(value) }.to_str() {
Ok(text) => Some(text),
Err(error) => {
set_last_error(format!("invalid UTF-8 in {name}: {error}"));
None
}
}
}