diff --git a/.changes/refactor-ipc-response.md b/.changes/refactor-ipc-response.md new file mode 100644 index 00000000000..f884d330a6c --- /dev/null +++ b/.changes/refactor-ipc-response.md @@ -0,0 +1,6 @@ +--- +"tauri": patch:breaking +--- + +Added a dedicated type for IPC response body `InvokeResponseBody` for performance reasons. +This is only a breaking change if you are directly using types from `tauri::ipc`. diff --git a/core/tauri-config-schema/schema.json b/core/tauri-config-schema/schema.json index 72cf80c655b..90c2831a36c 100644 --- a/core/tauri-config-schema/schema.json +++ b/core/tauri-config-schema/schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Config", - "description": "The Tauri configuration object.\n It is read from a file where you can define your frontend assets,\n configure the bundler and define a tray icon.\n\n The configuration file is generated by the\n [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in\n your Tauri application source directory (src-tauri).\n\n Once generated, you may modify it at will to customize your Tauri application.\n\n ## File Formats\n\n By default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\n Tauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively.\n The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`.\n The TOML file name is `Tauri.toml`.\n\n ## Platform-Specific Configuration\n\n In addition to the default configuration file, Tauri can\n read a platform-specific configuration from `tauri.linux.conf.json`,\n `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json`\n (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used),\n which gets merged with the main configuration object.\n\n ## Configuration Structure\n\n The configuration is composed of the following objects:\n\n - [`app`](#appconfig): The Tauri configuration\n - [`build`](#buildconfig): The build configuration\n - [`bundle`](#bundleconfig): The bundle configurations\n - [`plugins`](#pluginconfig): The plugins configuration\n\n ```json title=\"Example tauri.config.json file\"\n {\n \"productName\": \"tauri-app\",\n \"version\": \"0.1.0\",\n \"build\": {\n \"beforeBuildCommand\": \"\",\n \"beforeDevCommand\": \"\",\n \"devUrl\": \"../dist\",\n \"frontendDist\": \"../dist\"\n },\n \"app\": {\n \"security\": {\n \"csp\": null\n },\n \"windows\": [\n {\n \"fullscreen\": false,\n \"height\": 600,\n \"resizable\": true,\n \"title\": \"Tauri App\",\n \"width\": 800\n }\n ]\n },\n \"bundle\": {},\n \"plugins\": {}\n }\n ```", + "description": "The Tauri configuration object.\n It is read from a file where you can define your frontend assets,\n configure the bundler and define a tray icon.\n\n The configuration file is generated by the\n [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in\n your Tauri application source directory (src-tauri).\n\n Once generated, you may modify it at will to customize your Tauri application.\n\n ## File Formats\n\n By default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\n Tauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively.\n The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`.\n The TOML file name is `Tauri.toml`.\n\n ## Platform-Specific Configuration\n\n In addition to the default configuration file, Tauri can\n read a platform-specific configuration from `tauri.linux.conf.json`,\n `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json`\n (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used),\n which gets merged with the main configuration object.\n\n ## Configuration Structure\n\n The configuration is composed of the following objects:\n\n - [`app`](#appconfig): The Tauri configuration\n - [`build`](#buildconfig): The build configuration\n - [`bundle`](#bundleconfig): The bundle configurations\n - [`plugins`](#pluginconfig): The plugins configuration\n\n Example tauri.config.json file:\n\n ```json\n {\n \"productName\": \"tauri-app\",\n \"version\": \"0.1.0\",\n \"build\": {\n \"beforeBuildCommand\": \"\",\n \"beforeDevCommand\": \"\",\n \"devUrl\": \"../dist\",\n \"frontendDist\": \"../dist\"\n },\n \"app\": {\n \"security\": {\n \"csp\": null\n },\n \"windows\": [\n {\n \"fullscreen\": false,\n \"height\": 600,\n \"resizable\": true,\n \"title\": \"Tauri App\",\n \"width\": 800\n }\n ]\n },\n \"bundle\": {},\n \"plugins\": {}\n }\n ```", "type": "object", "properties": { "$schema": { diff --git a/core/tauri-runtime-wry/src/lib.rs b/core/tauri-runtime-wry/src/lib.rs index 56cf7900eec..51242e68839 100644 --- a/core/tauri-runtime-wry/src/lib.rs +++ b/core/tauri-runtime-wry/src/lib.rs @@ -46,8 +46,8 @@ use wry::WebViewBuilderExtWindows; use tao::{ dpi::{ LogicalPosition as TaoLogicalPosition, LogicalSize as TaoLogicalSize, - LogicalUnit as ToaLogicalUnit, PhysicalPosition as TaoPhysicalPosition, - PhysicalSize as TaoPhysicalSize, Position as TaoPosition, Size as TaoSize, + PhysicalPosition as TaoPhysicalPosition, PhysicalSize as TaoPhysicalSize, + Position as TaoPosition, Size as TaoSize, }, event::{Event, StartCause, WindowEvent as TaoWindowEvent}, event_loop::{ @@ -793,16 +793,16 @@ impl WindowBuilder for WindowBuilderWrapper { let mut constraints = WindowSizeConstraints::default(); if let Some(min_width) = config.min_width { - constraints.min_width = Some(ToaLogicalUnit::new(min_width).into()); + constraints.min_width = Some(tao::dpi::LogicalUnit::new(min_width).into()); } if let Some(min_height) = config.min_height { - constraints.min_height = Some(ToaLogicalUnit::new(min_height).into()); + constraints.min_height = Some(tao::dpi::LogicalUnit::new(min_height).into()); } if let Some(max_width) = config.max_width { - constraints.max_width = Some(ToaLogicalUnit::new(max_width).into()); + constraints.max_width = Some(tao::dpi::LogicalUnit::new(max_width).into()); } if let Some(max_height) = config.max_height { - constraints.max_height = Some(ToaLogicalUnit::new(max_height).into()); + constraints.max_height = Some(tao::dpi::LogicalUnit::new(max_height).into()); } window = window.inner_size_constraints(constraints); diff --git a/core/tauri-utils/src/config.rs b/core/tauri-utils/src/config.rs index 4ca1c613971..2089da33943 100644 --- a/core/tauri-utils/src/config.rs +++ b/core/tauri-utils/src/config.rs @@ -2170,7 +2170,9 @@ where /// - [`bundle`](#bundleconfig): The bundle configurations /// - [`plugins`](#pluginconfig): The plugins configuration /// -/// ```json title="Example tauri.config.json file" +/// Example tauri.config.json file: +/// +/// ```json /// { /// "productName": "tauri-app", /// "version": "0.1.0", diff --git a/core/tauri/src/app.rs b/core/tauri/src/app.rs index 1cda98bda80..b998d304424 100644 --- a/core/tauri/src/app.rs +++ b/core/tauri/src/app.rs @@ -1741,6 +1741,13 @@ tauri::Builder::default() self.invoke_key, )); + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] let app_id = if manager.config.app.enable_gtk_app_id { Some(manager.config.identifier.clone()) } else { diff --git a/core/tauri/src/ipc/channel.rs b/core/tauri/src/ipc/channel.rs index 922186fc4e2..8d01dd16a8d 100644 --- a/core/tauri/src/ipc/channel.rs +++ b/core/tauri/src/ipc/channel.rs @@ -20,7 +20,7 @@ use crate::{ Manager, Runtime, State, Webview, }; -use super::{CallbackFn, InvokeBody, InvokeError, IpcResponse, Request, Response}; +use super::{CallbackFn, InvokeError, InvokeResponseBody, IpcResponse, Request, Response}; pub const IPC_PAYLOAD_PREFIX: &str = "__CHANNEL__:"; pub const CHANNEL_PLUGIN_NAME: &str = "__TAURI_CHANNEL__"; @@ -33,13 +33,13 @@ static CHANNEL_DATA_COUNTER: AtomicU32 = AtomicU32::new(0); /// Maps a channel id to a pending data that must be send to the JavaScript side via the IPC. #[derive(Default, Clone)] -pub struct ChannelDataIpcQueue(pub(crate) Arc>>); +pub struct ChannelDataIpcQueue(pub(crate) Arc>>); /// An IPC channel. #[derive(Clone)] -pub struct Channel { +pub struct Channel { id: u32, - on_message: Arc crate::Result<()> + Send + Sync>, + on_message: Arc crate::Result<()> + Send + Sync>, phantom: std::marker::PhantomData, } @@ -138,13 +138,13 @@ impl<'de> Deserialize<'de> for JavaScriptChannelId { impl Channel { /// Creates a new channel with the given message handler. - pub fn new crate::Result<()> + Send + Sync + 'static>( + pub fn new crate::Result<()> + Send + Sync + 'static>( on_message: F, ) -> Self { Self::new_with_id(CHANNEL_COUNTER.fetch_add(1, Ordering::Relaxed), on_message) } - fn new_with_id crate::Result<()> + Send + Sync + 'static>( + fn new_with_id crate::Result<()> + Send + Sync + 'static>( id: u32, on_message: F, ) -> Self { @@ -195,8 +195,7 @@ impl Channel { where TSend: IpcResponse, { - let body = data.body()?; - (self.on_message)(body) + (self.on_message)(data.body()?) } } diff --git a/core/tauri/src/ipc/command.rs b/core/tauri/src/ipc/command.rs index dc99065cd27..af3596a89bc 100644 --- a/core/tauri/src/ipc/command.rs +++ b/core/tauri/src/ipc/command.rs @@ -183,7 +183,7 @@ impl<'de, R: Runtime> Deserializer<'de> for CommandItem<'de, R> { #[doc(hidden)] pub mod private { use crate::{ - ipc::{InvokeBody, InvokeError, InvokeResolver, IpcResponse}, + ipc::{InvokeError, InvokeResolver, InvokeResponseBody, IpcResponse}, Runtime, }; use futures_util::{FutureExt, TryFutureExt}; @@ -220,7 +220,10 @@ pub mod private { } #[inline(always)] - pub fn future(self, value: T) -> impl Future> + pub fn future( + self, + value: T, + ) -> impl Future> where T: IpcResponse, { @@ -261,7 +264,7 @@ pub mod private { pub fn future( self, value: Result, - ) -> impl Future> + ) -> impl Future> where T: IpcResponse, E: Into, @@ -288,7 +291,10 @@ pub mod private { impl FutureTag { #[inline(always)] - pub fn future(self, value: F) -> impl Future> + pub fn future( + self, + value: F, + ) -> impl Future> where T: IpcResponse, F: Future + Send + 'static, @@ -315,7 +321,10 @@ pub mod private { impl ResultFutureTag { #[inline(always)] - pub fn future(self, value: F) -> impl Future> + pub fn future( + self, + value: F, + ) -> impl Future> where T: IpcResponse, E: Into, diff --git a/core/tauri/src/ipc/format_callback.rs b/core/tauri/src/ipc/format_callback.rs index 4f603846525..0b75a82ac69 100644 --- a/core/tauri/src/ipc/format_callback.rs +++ b/core/tauri/src/ipc/format_callback.rs @@ -40,14 +40,14 @@ const MIN_JSON_PARSE_LEN: usize = 10_240; /// 1. `serde_json`'s ability to correctly escape and format json into a string. /// 2. JavaScript engines not accepting anything except another unescaped, literal single quote /// character to end a string that was opened with it. -fn serialize_js_with String>( - value: &T, +fn serialize_js_with String>( + json_string: String, options: serialize_to_javascript::Options, cb: F, ) -> crate::Result { // get a raw &str representation of a serialized json value. - let string = serde_json::to_string(value)?; - let raw = RawValue::from_string(string)?; + + let raw = RawValue::from_string(json_string)?; // from here we know json.len() > 1 because an empty string is not a valid json value. let json = raw.get(); @@ -77,14 +77,21 @@ fn serialize_js_with String>( Ok(return_val) } -/// Formats a function name and argument to be evaluated as callback. +/// Formats a function name and a serializable argument to be evaluated as callback. +/// +/// See [`format_raw`] for more information. +pub fn format(function_name: CallbackFn, arg: &T) -> crate::Result { + format_raw(function_name, serde_json::to_string(arg)?) +} + +/// Formats a function name and a raw JSON string argument to be evaluated as callback. /// /// This will serialize primitive JSON types (e.g. booleans, strings, numbers, etc.) as JavaScript literals, /// but will serialize arrays and objects whose serialized JSON string is smaller than 1 GB and larger /// than 10 KiB with `JSON.parse('...')`. /// See [json-parse-benchmark](https://github.com/GoogleChromeLabs/json-parse-benchmark). -pub fn format(function_name: CallbackFn, arg: &T) -> crate::Result { - serialize_js_with(arg, Default::default(), |arg| { +pub fn format_raw(function_name: CallbackFn, json_string: String) -> crate::Result { + serialize_js_with(json_string, Default::default(), |arg| { format!( r#" if (window["_{fn}"]) {{ @@ -97,7 +104,21 @@ pub fn format(function_name: CallbackFn, arg: &T) -> crate::Result }) } -/// Formats a Result type to its Promise response. +/// Formats a serializable Result type to its Promise response. +/// +/// See [`format_result_raw`] for more information. +pub fn format_result( + result: Result, + success_callback: CallbackFn, + error_callback: CallbackFn, +) -> crate::Result { + match result { + Ok(res) => format(success_callback, &res), + Err(err) => format(error_callback, &err), + } +} + +/// Formats a Result type of raw JSON strings to its Promise response. /// Useful for Promises handling. /// If the Result `is_ok()`, the callback will be the `success_callback` function name and the argument will be the Ok value. /// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value. @@ -107,14 +128,14 @@ pub fn format(function_name: CallbackFn, arg: &T) -> crate::Result /// * `error_callback` the function name of the Err callback. Usually the `reject` of the JS Promise. /// /// Note that the callback strings are automatically generated by the `invoke` helper. -pub fn format_result( - result: Result, +pub fn format_result_raw( + raw_result: Result, success_callback: CallbackFn, error_callback: CallbackFn, ) -> crate::Result { - match result { - Ok(res) => format(success_callback, &res), - Err(err) => format(error_callback, &err), + match raw_result { + Ok(res) => format_raw(success_callback, res), + Err(err) => format_raw(error_callback, err), } } @@ -130,8 +151,31 @@ mod test { } } + #[derive(Debug, Clone)] + struct JsonStr(String); + + impl Arbitrary for JsonStr { + fn arbitrary(g: &mut Gen) -> Self { + if bool::arbitrary(g) { + Self(format!( + "{{ {}: {} }}", + serde_json::to_string(&String::arbitrary(g)).unwrap(), + serde_json::to_string(&String::arbitrary(g)).unwrap() + )) + } else { + Self(serde_json::to_string(&String::arbitrary(g)).unwrap()) + } + } + } + fn serialize_js(value: &T) -> crate::Result { - serialize_js_with(value, Default::default(), |v| v.into()) + serialize_js_with(serde_json::to_string(value)?, Default::default(), |v| { + v.into() + }) + } + + fn serialize_js_raw(value: impl Into) -> crate::Result { + serialize_js_with(value.into(), Default::default(), |v| v.into()) } #[test] @@ -213,4 +257,79 @@ mod test { serde_json::Value::String(value), )) } + + #[test] + fn test_serialize_js_raw() { + assert_eq!(serialize_js_raw("null").unwrap(), "null"); + assert_eq!(serialize_js_raw("5").unwrap(), "5"); + assert_eq!( + serialize_js_raw("{ \"x\": [1, 2, 3] }").unwrap(), + "{ \"x\": [1, 2, 3] }" + ); + + #[derive(serde::Serialize)] + struct JsonObj { + value: String, + } + + let raw_str = "T".repeat(MIN_JSON_PARSE_LEN); + assert_eq!( + serialize_js_raw(format!("\"{raw_str}\"")).unwrap(), + format!("\"{raw_str}\"") + ); + + assert_eq!( + serialize_js_raw(format!("{{\"value\":\"{raw_str}\"}}")).unwrap(), + format!("JSON.parse('{{\"value\":\"{raw_str}\"}}')") + ); + + assert_eq!( + serialize_js(&JsonObj { + value: format!("\"{raw_str}\"") + }) + .unwrap(), + format!("JSON.parse('{{\"value\":\"\\\\\"{raw_str}\\\\\"\"}}')") + ); + + let dangerous_json = RawValue::from_string( + r#"{"test":"don\\πŸš€πŸ±β€πŸ‘€\\'t forget to escape me!πŸš€πŸ±β€πŸ‘€","teπŸš€πŸ±β€πŸ‘€st2":"don't forget to escape me!","test3":"\\πŸš€πŸ±β€πŸ‘€\\\\'''\\\\πŸš€πŸ±β€πŸ‘€\\\\πŸš€πŸ±β€πŸ‘€\\'''''"}"#.into() + ).unwrap(); + + let definitely_escaped_dangerous_json = format!( + "JSON.parse('{}')", + dangerous_json + .get() + .replace('\\', "\\\\") + .replace('\'', "\\'") + ); + let escape_single_quoted_json_test = + serialize_to_javascript::Serialized::new(&dangerous_json, &Default::default()).into_string(); + + let result = r#"JSON.parse('{"test":"don\\\\πŸš€πŸ±β€πŸ‘€\\\\\'t forget to escape me!πŸš€πŸ±β€πŸ‘€","teπŸš€πŸ±β€πŸ‘€st2":"don\'t forget to escape me!","test3":"\\\\πŸš€πŸ±β€πŸ‘€\\\\\\\\\'\'\'\\\\\\\\πŸš€πŸ±β€πŸ‘€\\\\\\\\πŸš€πŸ±β€πŸ‘€\\\\\'\'\'\'\'"}')"#; + assert_eq!(definitely_escaped_dangerous_json, result); + assert_eq!(escape_single_quoted_json_test, result); + } + + // check arbitrary strings in the format callback function + #[quickcheck] + fn qc_formatting_raw(f: CallbackFn, a: JsonStr) -> bool { + let a = a.0; + // call format callback + let fc = format_raw(f, a.clone()).unwrap(); + fc.contains(&format!(r#"window["_{}"](JSON.parse('{}'))"#, f.0, a)) + || fc.contains(&format!(r#"window["_{}"]({})"#, f.0, a)) + } + + // check arbitrary strings in format_result + #[quickcheck] + fn qc_format_raw_res(result: Result, c: CallbackFn, ec: CallbackFn) -> bool { + let result = result.map(|v| v.0).map_err(|e| e.0); + let resp = format_result_raw(result.clone(), c, ec).expect("failed to format callback result"); + let (function, value) = match result { + Ok(v) => (c, v), + Err(e) => (ec, e), + }; + + resp.contains(&format!(r#"window["_{}"]({})"#, function.0, value)) + } } diff --git a/core/tauri/src/ipc/mod.rs b/core/tauri/src/ipc/mod.rs index ca23f182925..05442f3cf9a 100644 --- a/core/tauri/src/ipc/mod.rs +++ b/core/tauri/src/ipc/mod.rs @@ -72,14 +72,8 @@ impl From> for InvokeBody { } } -impl IpcResponse for InvokeBody { - fn body(self) -> crate::Result { - Ok(self) - } -} - impl InvokeBody { - #[allow(dead_code)] + #[cfg(mobile)] pub(crate) fn into_json(self) -> JsonValue { match self { Self::Json(v) => v, @@ -88,12 +82,51 @@ impl InvokeBody { } } } +} + +/// Possible values of an IPC response. +#[derive(Debug, Clone)] +#[cfg_attr(test, derive(PartialEq))] +pub enum InvokeResponseBody { + /// Json payload. + Json(String), + /// Bytes payload. + Raw(Vec), +} + +impl From for InvokeResponseBody { + fn from(value: String) -> Self { + Self::Json(value) + } +} + +impl From> for InvokeResponseBody { + fn from(value: Vec) -> Self { + Self::Raw(value) + } +} + +impl From for InvokeResponseBody { + fn from(value: InvokeBody) -> Self { + match value { + InvokeBody::Json(v) => Self::Json(serde_json::to_string(&v).unwrap()), + InvokeBody::Raw(v) => Self::Raw(v), + } + } +} + +impl IpcResponse for InvokeResponseBody { + fn body(self) -> crate::Result { + Ok(self) + } +} - /// Attempts to deserialize the invoke body. +impl InvokeResponseBody { + /// Attempts to deserialize the response. pub fn deserialize(self) -> serde_json::Result { match self { - InvokeBody::Json(v) => serde_json::from_value(v), - InvokeBody::Raw(v) => T::deserialize(v.into_deserializer()), + Self::Json(v) => serde_json::from_str(&v), + Self::Raw(v) => T::deserialize(v.into_deserializer()), } } } @@ -130,12 +163,12 @@ impl<'a, R: Runtime> CommandArg<'a, R> for Request<'a> { /// Marks a type as a response to an IPC call. pub trait IpcResponse { /// Resolve the IPC response body. - fn body(self) -> crate::Result; + fn body(self) -> crate::Result; } impl IpcResponse for T { - fn body(self) -> crate::Result { - serde_json::to_value(self) + fn body(self) -> crate::Result { + serde_json::to_string(&self) .map(Into::into) .map_err(Into::into) } @@ -143,18 +176,18 @@ impl IpcResponse for T { /// The IPC request. pub struct Response { - body: InvokeBody, + body: InvokeResponseBody, } impl IpcResponse for Response { - fn body(self) -> crate::Result { + fn body(self) -> crate::Result { Ok(self.body) } } impl Response { /// Defines a response with the given body. - pub fn new(body: impl Into) -> Self { + pub fn new(body: impl Into) -> Self { Self { body: body.into() } } } @@ -177,19 +210,19 @@ pub struct Invoke { /// Error response from an [`InvokeMessage`]. #[derive(Debug)] -pub struct InvokeError(pub JsonValue); +pub struct InvokeError(pub serde_json::Value); impl InvokeError { /// Create an [`InvokeError`] as a string of the [`std::error::Error`] message. #[inline(always)] pub fn from_error(error: E) -> Self { - Self(JsonValue::String(error.to_string())) + Self(serde_json::Value::String(error.to_string())) } /// Create an [`InvokeError`] as a string of the [`anyhow::Error`] message. #[inline(always)] pub fn from_anyhow(error: anyhow::Error) -> Self { - Self(JsonValue::String(format!("{error:#}"))) + Self(serde_json::Value::String(format!("{error:#}"))) } } @@ -205,7 +238,7 @@ impl From for InvokeError { impl From for InvokeError { #[inline(always)] fn from(error: crate::Error) -> Self { - Self(JsonValue::String(error.to_string())) + Self(serde_json::Value::String(error.to_string())) } } @@ -213,24 +246,11 @@ impl From for InvokeError { #[derive(Debug)] pub enum InvokeResponse { /// Resolve the promise. - Ok(InvokeBody), + Ok(InvokeResponseBody), /// Reject the promise. Err(InvokeError), } -impl Serialize for InvokeResponse { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - match self { - Self::Ok(InvokeBody::Json(j)) => j.serialize(serializer), - Self::Ok(InvokeBody::Raw(b)) => b.serialize(serializer), - Self::Err(e) => e.0.serialize(serializer), - } - } -} - impl> From> for InvokeResponse { #[inline] fn from(result: Result) -> Self { @@ -311,7 +331,7 @@ impl InvokeResolver { /// Reply to the invoke promise with an async task which is already serialized. pub fn respond_async_serialized(self, task: F) where - F: Future> + Send + 'static, + F: Future> + Send + 'static, { crate::async_runtime::spawn(async move { let response = match task.await { @@ -531,22 +551,18 @@ mod tests { use super::*; #[test] - fn deserialize_invoke_body() { - let json = InvokeBody::Json(serde_json::Value::Array(vec![ - serde_json::Value::Number(1.into()), - serde_json::Value::Number(123.into()), - serde_json::Value::Number(1231.into()), - ])); + fn deserialize_invoke_response_body() { + let json = InvokeResponseBody::Json("[1, 123, 1231]".to_string()); assert_eq!(json.deserialize::>().unwrap(), vec![1, 123, 1231]); - let json = InvokeBody::Json(serde_json::Value::String("string value".into())); + let json = InvokeResponseBody::Json("\"string value\"".to_string()); assert_eq!(json.deserialize::().unwrap(), "string value"); - let json = InvokeBody::Json(serde_json::Value::String("string value".into())); + let json = InvokeResponseBody::Json("\"string value\"".to_string()); assert!(json.deserialize::>().is_err()); let values = vec![1, 2, 3, 4, 5, 6, 1]; - let raw = InvokeBody::Raw(values.clone()); + let raw = InvokeResponseBody::Raw(values.clone()); assert_eq!(raw.deserialize::>().unwrap(), values); } } diff --git a/core/tauri/src/ipc/protocol.rs b/core/tauri/src/ipc/protocol.rs index 7698c0fe0df..ef25f654255 100644 --- a/core/tauri/src/ipc/protocol.rs +++ b/core/tauri/src/ipc/protocol.rs @@ -5,6 +5,7 @@ use std::{borrow::Cow, sync::Arc}; use crate::{ + ipc::InvokeResponseBody, manager::AppManager, webview::{InvokeRequest, UriSchemeProtocolHandler}, Runtime, @@ -18,7 +19,7 @@ use http::{ }; use url::Url; -use super::{CallbackFn, InvokeBody, InvokeResponse}; +use super::{CallbackFn, InvokeResponse}; const TAURI_CALLBACK_HEADER_NAME: &str = "Tauri-Callback"; const TAURI_ERROR_HEADER_NAME: &str = "Tauri-Error"; @@ -67,8 +68,8 @@ pub fn get(manager: Arc>, label: String) -> UriSchemeP span.record( "request", match &request.body { - InvokeBody::Json(j) => serde_json::to_string(j).unwrap(), - InvokeBody::Raw(b) => serde_json::to_string(b).unwrap(), + super::InvokeBody::Json(j) => serde_json::to_string(j).unwrap(), + super::InvokeBody::Raw(b) => serde_json::to_string(b).unwrap(), }, ); #[cfg(feature = "tracing")] @@ -85,12 +86,26 @@ pub fn get(manager: Arc>, label: String) -> UriSchemeP .entered(); #[cfg(feature = "tracing")] - let response_span = tracing::trace_span!( - "ipc::request::response", - response = serde_json::to_string(&response).unwrap(), - mime_type = tracing::field::Empty - ) - .entered(); + let response_span = match &response { + InvokeResponse::Ok(InvokeResponseBody::Json(v)) => tracing::trace_span!( + "ipc::request::response", + response = v, + mime_type = tracing::field::Empty + ) + .entered(), + InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => tracing::trace_span!( + "ipc::request::response", + response = format!("{v:?}"), + mime_type = tracing::field::Empty + ) + .entered(), + InvokeResponse::Err(e) => tracing::trace_span!( + "ipc::request::response", + error = format!("{e:?}"), + mime_type = tracing::field::Empty + ) + .entered(), + }; let response_header = match &response { InvokeResponse::Ok(_) => TAURI_RESPONSE_HEADER_OK, @@ -98,11 +113,11 @@ pub fn get(manager: Arc>, label: String) -> UriSchemeP }; let (mut response, mime_type) = match response { - InvokeResponse::Ok(InvokeBody::Json(v)) => ( - http::Response::new(serde_json::to_vec(&v).unwrap().into()), + InvokeResponse::Ok(InvokeResponseBody::Json(v)) => ( + http::Response::new(v.as_bytes().to_vec().into()), mime::APPLICATION_JSON, ), - InvokeResponse::Ok(InvokeBody::Raw(v)) => ( + InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => ( http::Response::new(v.into()), mime::APPLICATION_OCTET_STREAM, ), @@ -302,14 +317,8 @@ fn handle_ipc_message(request: Request, manager: &AppManager webview.on_message( request, Box::new(move |webview, cmd, response, callback, error| { - use crate::ipc::{ - format_callback::{ - format as format_callback, format_result as format_callback_result, - }, - Channel, - }; + use crate::ipc::Channel; use crate::sealed::ManagerBase; - use serde_json::Value as JsonValue; #[cfg(feature = "tracing")] let _respond_span = tracing::trace_span!( @@ -327,7 +336,7 @@ fn handle_ipc_message(request: Request, manager: &AppManager ) { let eval_js = match js { Ok(js) => js, - Err(e) => format_callback(error, &e.to_string()) + Err(e) => crate::ipc::format_callback::format(error, &e.to_string()) .expect("unable to serialize response error string to json"), }; @@ -339,51 +348,80 @@ fn handle_ipc_message(request: Request, manager: &AppManager && !options.custom_protocol_ipc_blocked; #[cfg(feature = "tracing")] - let _response_span = tracing::trace_span!( - "ipc::request::response", - response = serde_json::to_string(&response).unwrap(), - mime_type = match &response { - InvokeResponse::Ok(InvokeBody::Json(_)) => mime::APPLICATION_JSON, - InvokeResponse::Ok(InvokeBody::Raw(_)) => mime::APPLICATION_OCTET_STREAM, - InvokeResponse::Err(_) => mime::APPLICATION_JSON, - } - .essence_str() - ) - .entered(); + let mime_type = match &response { + InvokeResponse::Ok(InvokeResponseBody::Json(_)) => mime::APPLICATION_JSON, + InvokeResponse::Ok(InvokeResponseBody::Raw(_)) => mime::APPLICATION_OCTET_STREAM, + InvokeResponse::Err(_) => mime::APPLICATION_JSON, + }; - match &response { - InvokeResponse::Ok(InvokeBody::Json(v)) => { + #[cfg(feature = "tracing")] + let _response_span = match &response { + InvokeResponse::Ok(InvokeResponseBody::Json(v)) => tracing::trace_span!( + "ipc::request::response", + response = v, + mime_type = mime_type.essence_str() + ) + .entered(), + InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => tracing::trace_span!( + "ipc::request::response", + response = format!("{v:?}"), + mime_type = mime_type.essence_str() + ) + .entered(), + InvokeResponse::Err(e) => tracing::trace_span!( + "ipc::request::response", + response = format!("{e:?}"), + mime_type = mime_type.essence_str() + ) + .entered(), + }; + + match response { + InvokeResponse::Ok(InvokeResponseBody::Json(v)) => { if !(cfg!(target_os = "macos") || cfg!(target_os = "ios")) - && matches!(v, JsonValue::Object(_) | JsonValue::Array(_)) + && (v.starts_with('{') || v.starts_with('[')) && can_use_channel_for_response { - let _ = Channel::from_callback_fn(webview, callback).send(v); + let _ = Channel::from_callback_fn(webview, callback) + .send(InvokeResponseBody::Json(v)); } else { responder_eval( &webview, - format_callback_result(Result::<_, ()>::Ok(v), callback, error), + crate::ipc::format_callback::format_result_raw( + Result::<_, String>::Ok(v), + callback, + error, + ), error, ) } } - InvokeResponse::Ok(InvokeBody::Raw(v)) => { + InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => { if cfg!(target_os = "macos") || cfg!(target_os = "ios") || !can_use_channel_for_response { responder_eval( &webview, - format_callback_result(Result::<_, ()>::Ok(v), callback, error), + crate::ipc::format_callback::format_result( + Result::<_, ()>::Ok(v), + callback, + error, + ), error, ); } else { - let _ = - Channel::from_callback_fn(webview, callback).send(InvokeBody::Raw(v.clone())); + let _ = Channel::from_callback_fn(webview, callback) + .send(InvokeResponseBody::Raw(v.clone())); } } InvokeResponse::Err(e) => responder_eval( &webview, - format_callback_result(Result::<(), _>::Err(&e.0), callback, error), + crate::ipc::format_callback::format_result( + Result::<(), _>::Err(&e.0), + callback, + error, + ), error, ), } @@ -530,7 +568,7 @@ mod tests { use std::str::FromStr; use super::*; - use crate::{manager::AppManager, plugin::PluginStore, StateManager, Wry}; + use crate::{ipc::InvokeBody, manager::AppManager, plugin::PluginStore, StateManager, Wry}; use http::header::*; use serde_json::json; use tauri_macros::generate_context; diff --git a/core/tauri/src/manager/mod.rs b/core/tauri/src/manager/mod.rs index 8210706d94d..b095086234b 100644 --- a/core/tauri/src/manager/mod.rs +++ b/core/tauri/src/manager/mod.rs @@ -615,6 +615,7 @@ impl AppManager { } } + #[cfg(desktop)] pub(crate) fn on_webview_close(&self, label: &str) { self.webview.webviews_lock().remove(label); diff --git a/core/tauri/src/test/mock_runtime.rs b/core/tauri/src/test/mock_runtime.rs index 1123fccb0ad..449451b7d40 100644 --- a/core/tauri/src/test/mock_runtime.rs +++ b/core/tauri/src/test/mock_runtime.rs @@ -228,7 +228,7 @@ impl RuntimeHandle for MockRuntimeHandle { )) }); #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))] - return unimplemented!(); + unimplemented!(); } fn primary_monitor(&self) -> Option { @@ -725,7 +725,7 @@ impl WindowDispatch for MockWindowDispatcher { )) }; #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))] - return unimplemented!(); + unimplemented!(); } fn center(&self) -> Result<()> { diff --git a/core/tauri/src/test/mod.rs b/core/tauri/src/test/mod.rs index c051096522f..76d089a29cc 100644 --- a/core/tauri/src/test/mod.rs +++ b/core/tauri/src/test/mod.rs @@ -58,7 +58,7 @@ use serialize_to_javascript::DefaultTemplate; use std::{borrow::Cow, collections::HashMap, fmt::Debug}; use crate::{ - ipc::{InvokeBody, InvokeError, InvokeResponse, RuntimeAuthority}, + ipc::{InvokeError, InvokeResponse, InvokeResponseBody, RuntimeAuthority}, webview::InvokeRequest, App, Assets, Builder, Context, Pattern, Runtime, Webview, }; @@ -282,7 +282,7 @@ pub fn assert_ipc_response< pub fn get_ipc_response>>( webview: &W, request: InvokeRequest, -) -> Result { +) -> Result { let (tx, rx) = std::sync::mpsc::sync_channel(1); webview.as_ref().clone().on_message( request, diff --git a/core/tauri/src/webview/mod.rs b/core/tauri/src/webview/mod.rs index 3bf5fe39125..0c1be3164f6 100644 --- a/core/tauri/src/webview/mod.rs +++ b/core/tauri/src/webview/mod.rs @@ -20,7 +20,7 @@ use tauri_runtime::{ }; use tauri_runtime::{ webview::{DetachedWebview, PendingWebview, WebviewAttributes}, - Rect, WebviewDispatch, + WebviewDispatch, }; use tauri_utils::config::{WebviewUrl, WindowConfig}; pub use url::Url; @@ -605,7 +605,7 @@ tauri::Builder::default() let mut pending = self.into_pending_webview(&window, window.label())?; - pending.webview_attributes.bounds = Some(Rect { size, position }); + pending.webview_attributes.bounds = Some(tauri_runtime::Rect { size, position }); let webview = match &mut window.runtime() { RuntimeOrDispatch::Dispatch(dispatcher) => dispatcher.create_webview(pending), @@ -902,7 +902,7 @@ impl Webview { } /// Resizes this webview. - pub fn set_bounds(&self, bounds: Rect) -> crate::Result<()> { + pub fn set_bounds(&self, bounds: tauri_runtime::Rect) -> crate::Result<()> { self .webview .dispatcher @@ -958,7 +958,7 @@ impl Webview { } /// Returns the bounds of the webviews's client area. - pub fn bounds(&self) -> crate::Result { + pub fn bounds(&self) -> crate::Result { self.webview.dispatcher.bounds().map_err(Into::into) } diff --git a/core/tauri/src/webview/webview_window.rs b/core/tauri/src/webview/webview_window.rs index 6e4fd045bf7..ba0f8ce7a4c 100644 --- a/core/tauri/src/webview/webview_window.rs +++ b/core/tauri/src/webview/webview_window.rs @@ -27,7 +27,6 @@ use crate::{ }, }; use serde::Serialize; -use tauri_runtime::window::WindowSizeConstraints; use tauri_utils::config::{WebviewUrl, WindowConfig}; use url::Url; @@ -396,7 +395,10 @@ impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { /// Window inner size constraints. #[must_use] - pub fn inner_size_constraints(mut self, constraints: WindowSizeConstraints) -> Self { + pub fn inner_size_constraints( + mut self, + constraints: tauri_runtime::window::WindowSizeConstraints, + ) -> Self { self.window_builder = self.window_builder.inner_size_constraints(constraints); self } @@ -1465,7 +1467,10 @@ impl WebviewWindow { } /// Sets this window's minimum inner width. - pub fn set_size_constraints(&self, constriants: WindowSizeConstraints) -> crate::Result<()> { + pub fn set_size_constraints( + &self, + constriants: tauri_runtime::window::WindowSizeConstraints, + ) -> crate::Result<()> { self.webview.window().set_size_constraints(constriants) } diff --git a/core/tauri/src/window/mod.rs b/core/tauri/src/window/mod.rs index 6531cbd4503..fb3c2e8e2a4 100644 --- a/core/tauri/src/window/mod.rs +++ b/core/tauri/src/window/mod.rs @@ -9,7 +9,6 @@ pub(crate) mod plugin; use tauri_runtime::{ dpi::{PhysicalPosition, PhysicalSize}, webview::PendingWebview, - window::WindowSizeConstraints, }; pub use tauri_utils::{config::Color, WindowEffect as Effect, WindowEffectState as EffectState}; @@ -461,7 +460,10 @@ impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { /// Window inner size constraints. #[must_use] - pub fn inner_size_constraints(mut self, constraints: WindowSizeConstraints) -> Self { + pub fn inner_size_constraints( + mut self, + constraints: tauri_runtime::window::WindowSizeConstraints, + ) -> Self { self.window_builder = self.window_builder.inner_size_constraints(constraints); self } @@ -1830,7 +1832,10 @@ tauri::Builder::default() } /// Sets this window's minimum inner width. - pub fn set_size_constraints(&self, constriants: WindowSizeConstraints) -> crate::Result<()> { + pub fn set_size_constraints( + &self, + constriants: tauri_runtime::window::WindowSizeConstraints, + ) -> crate::Result<()> { self .window .dispatcher diff --git a/tooling/cli/schema.json b/tooling/cli/schema.json index 72cf80c655b..90c2831a36c 100644 --- a/tooling/cli/schema.json +++ b/tooling/cli/schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Config", - "description": "The Tauri configuration object.\n It is read from a file where you can define your frontend assets,\n configure the bundler and define a tray icon.\n\n The configuration file is generated by the\n [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in\n your Tauri application source directory (src-tauri).\n\n Once generated, you may modify it at will to customize your Tauri application.\n\n ## File Formats\n\n By default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\n Tauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively.\n The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`.\n The TOML file name is `Tauri.toml`.\n\n ## Platform-Specific Configuration\n\n In addition to the default configuration file, Tauri can\n read a platform-specific configuration from `tauri.linux.conf.json`,\n `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json`\n (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used),\n which gets merged with the main configuration object.\n\n ## Configuration Structure\n\n The configuration is composed of the following objects:\n\n - [`app`](#appconfig): The Tauri configuration\n - [`build`](#buildconfig): The build configuration\n - [`bundle`](#bundleconfig): The bundle configurations\n - [`plugins`](#pluginconfig): The plugins configuration\n\n ```json title=\"Example tauri.config.json file\"\n {\n \"productName\": \"tauri-app\",\n \"version\": \"0.1.0\",\n \"build\": {\n \"beforeBuildCommand\": \"\",\n \"beforeDevCommand\": \"\",\n \"devUrl\": \"../dist\",\n \"frontendDist\": \"../dist\"\n },\n \"app\": {\n \"security\": {\n \"csp\": null\n },\n \"windows\": [\n {\n \"fullscreen\": false,\n \"height\": 600,\n \"resizable\": true,\n \"title\": \"Tauri App\",\n \"width\": 800\n }\n ]\n },\n \"bundle\": {},\n \"plugins\": {}\n }\n ```", + "description": "The Tauri configuration object.\n It is read from a file where you can define your frontend assets,\n configure the bundler and define a tray icon.\n\n The configuration file is generated by the\n [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in\n your Tauri application source directory (src-tauri).\n\n Once generated, you may modify it at will to customize your Tauri application.\n\n ## File Formats\n\n By default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\n Tauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively.\n The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`.\n The TOML file name is `Tauri.toml`.\n\n ## Platform-Specific Configuration\n\n In addition to the default configuration file, Tauri can\n read a platform-specific configuration from `tauri.linux.conf.json`,\n `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json`\n (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used),\n which gets merged with the main configuration object.\n\n ## Configuration Structure\n\n The configuration is composed of the following objects:\n\n - [`app`](#appconfig): The Tauri configuration\n - [`build`](#buildconfig): The build configuration\n - [`bundle`](#bundleconfig): The bundle configurations\n - [`plugins`](#pluginconfig): The plugins configuration\n\n Example tauri.config.json file:\n\n ```json\n {\n \"productName\": \"tauri-app\",\n \"version\": \"0.1.0\",\n \"build\": {\n \"beforeBuildCommand\": \"\",\n \"beforeDevCommand\": \"\",\n \"devUrl\": \"../dist\",\n \"frontendDist\": \"../dist\"\n },\n \"app\": {\n \"security\": {\n \"csp\": null\n },\n \"windows\": [\n {\n \"fullscreen\": false,\n \"height\": 600,\n \"resizable\": true,\n \"title\": \"Tauri App\",\n \"width\": 800\n }\n ]\n },\n \"bundle\": {},\n \"plugins\": {}\n }\n ```", "type": "object", "properties": { "$schema": {