use serde::Deserialize; use serde_json::{json, Value}; use std::ffi::{c_char, CStr, CString}; use std::sync::OnceLock; use tauri::{AppHandle, Url}; static APP: OnceLock = OnceLock::new(); #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct Request { pub version: u8, pub id: u64, pub action: String, pub payload: Value, } #[derive(Debug, Deserialize)] #[serde( tag = "action", content = "payload", rename_all = "camelCase", deny_unknown_fields )] pub enum Action { GetState {}, SettingsReady {}, OpenSettings {}, SetWorkspace { server: String, }, CheckForUpdates {}, SetUpdatePreferences { automatic: bool, #[serde(rename = "intervalHours")] interval_hours: u64, }, InstallUpdate { version: String, }, } pub fn parse(value: &str) -> Result<(u64, Action), &'static str> { if value.len() > 8192 { return Err("invalidRequest"); } let request: Request = serde_json::from_str(value).map_err(|_| "invalidRequest")?; if request.version != 1 { return Err("incompatibleVersion"); } let action = serde_json::from_value(json!({"action": request.action, "payload": request.payload})) .map_err(|_| "invalidRequest")?; Ok((request.id, action)) } pub fn install(app: &AppHandle, window: &tauri::WebviewWindow) -> tauri::Result<()> { let _ = APP.set(app.clone()); let label = CString::new(window.label()).unwrap(); let script = CString::new(include_str!("../../ui/workspace-settings.js")).unwrap(); window.with_webview(move |view| unsafe { unsafe extern "C" { fn install_workspace_settings( view: *mut std::ffi::c_void, label: *const c_char, script: *const c_char, callback: extern "C" fn( *const c_char, *const c_char, *const c_char, bool, ) -> *mut c_char, free: extern "C" fn(*mut c_char), ); } install_workspace_settings( view.inner().cast(), label.as_ptr(), script.as_ptr(), receive, release, ); }) } extern "C" fn release(value: *mut c_char) { if !value.is_null() { unsafe { drop(CString::from_raw(value)); } } } extern "C" fn receive( label: *const c_char, url: *const c_char, body: *const c_char, main: bool, ) -> *mut c_char { let result = std::panic::catch_unwind(|| -> Result { if label.is_null() || url.is_null() || body.is_null() { return Err("invalidRequest"); } let label = unsafe { CStr::from_ptr(label) } .to_str() .map_err(|_| "invalidRequest")?; let url = unsafe { CStr::from_ptr(url) } .to_str() .map_err(|_| "invalidRequest")?; let body = unsafe { CStr::from_ptr(body) } .to_str() .map_err(|_| "invalidRequest")?; let app = APP.get().ok_or("bridgeUnavailable")?; let url = Url::parse(url).map_err(|_| "untrustedFrame")?; if !crate::desktop::allows_settings(app, label, &url, main) { return Err("untrustedFrame"); } let (id, action) = parse(body)?; let result = crate::desktop::settings_action(app, action)?; Ok(json!({"id": id, "result": result})) }) .unwrap_or(Err("bridgeUnavailable")); let value = result.unwrap_or_else(|error| json!({"error": error})); CString::new(value.to_string()).unwrap().into_raw() } #[cfg(test)] mod tests { use super::*; #[test] fn protocol_rejects_unknown_versions_actions_and_malformed_payloads() { for value in [ r#"{"version":2,"id":1,"action":"getState","payload":{}}"#, r#"{"version":1,"id":1,"action":"shell","payload":{}}"#, r#"{"version":1,"id":1,"action":"setWorkspace","payload":{"server":3}}"#, r#"{"version":1,"id":1,"action":"setUpdatePreferences","payload":{"automatic":"true","intervalHours":6}}"#, r#"{"version":1,"id":1,"action":"installUpdate","payload":{"version":"1.2.3","url":"https://evil.test"}}"#, ] { assert!(parse(value).is_err(), "{value}"); } assert!(parse(&"x".repeat(8193)).is_err()); assert!(matches!( parse(r#"{"version":1,"id":1,"action":"getState","payload":{}}"#), Ok((1, Action::GetState {})) )); } }