ipc.rs 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. // Copyright 2019-2021 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. //! Types and functions related to Inter Procedure Call(IPC).
  5. //!
  6. //! This module includes utilities to send messages to the JS layer of the webview.
  7. use serde::{Deserialize, Serialize};
  8. use serde_json::value::RawValue;
  9. /// The `Callback` type is the return value of the `transformCallback` JavaScript function.
  10. #[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
  11. pub struct CallbackFn(pub usize);
  12. /// The information about this is quite limited. On Chrome/Edge and Firefox, [the maximum string size is approximately 1 GB](https://stackoverflow.com/a/34958490).
  13. ///
  14. /// [From MDN:](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length#description)
  15. ///
  16. /// ECMAScript 2016 (ed. 7) established a maximum length of 2^53 - 1 elements. Previously, no maximum length was specified.
  17. ///
  18. /// In Firefox, strings have a maximum length of 2\*\*30 - 2 (~1GB). In versions prior to Firefox 65, the maximum length was 2\*\*28 - 1 (~256MB).
  19. const MAX_JSON_STR_LEN: usize = usize::pow(2, 30) - 2;
  20. /// Minimum size JSON needs to be in order to convert it to JSON.parse with [`escape_json_parse`].
  21. // TODO: this number should be benchmarked and checked for optimal range, I set 10 KiB arbitrarily
  22. // we don't want to lose the gained object parsing time to extra allocations preparing it
  23. const MIN_JSON_PARSE_LEN: usize = 10_240;
  24. /// Transforms & escapes a JSON String -> JSON.parse('{json}')
  25. ///
  26. /// Single quotes chosen because double quotes are already used in JSON. With single quotes, we only
  27. /// need to escape strings that include backslashes or single quotes. If we used double quotes, then
  28. /// there would be no cases that a string doesn't need escaping.
  29. ///
  30. /// # Safety
  31. ///
  32. /// The ability to safely escape JSON into a JSON.parse('{json}') relies entirely on 2 things.
  33. ///
  34. /// 1. `serde_json`'s ability to correctly escape and format json into a string.
  35. /// 2. JavaScript engines not accepting anything except another unescaped, literal single quote
  36. /// character to end a string that was opened with it.
  37. fn escape_json_parse(json: &RawValue) -> String {
  38. let json = json.get();
  39. // 14 chars in JSON.parse('')
  40. // todo: should we increase the 14 by x to allow x amount of escapes before another allocation?
  41. let mut s = String::with_capacity(json.len() + 14);
  42. s.push_str("JSON.parse('");
  43. // insert a backslash before any backslash or single quote characters.
  44. let mut last = 0;
  45. for (idx, _) in json.match_indices(|c| c == '\\' || c == '\'') {
  46. s.push_str(&json[last..idx]);
  47. s.push('\\');
  48. last = idx;
  49. }
  50. // finish appending the trailing characters that don't need escaping
  51. s.push_str(&json[last..]);
  52. s.push_str("')");
  53. s
  54. }
  55. /// Formats a function name and argument to be evaluated as callback.
  56. ///
  57. /// This will serialize primitive JSON types (e.g. booleans, strings, numbers, etc.) as JavaScript literals,
  58. /// but will serialize arrays and objects whose serialized JSON string is smaller than 1 GB and larger
  59. /// than 10 KiB with `JSON.parse('...')`.
  60. /// See [json-parse-benchmark](https://github.com/GoogleChromeLabs/json-parse-benchmark).
  61. ///
  62. /// # Examples
  63. /// - With string literals:
  64. /// ```
  65. /// use tauri::api::ipc::{CallbackFn, format_callback};
  66. /// // callback with a string argument
  67. /// let cb = format_callback(CallbackFn(12345), &"the string response").unwrap();
  68. /// assert!(cb.contains(r#"window["_12345"]("the string response")"#));
  69. /// ```
  70. ///
  71. /// - With types implement [`serde::Serialize`]:
  72. /// ```
  73. /// use tauri::api::ipc::{CallbackFn, format_callback};
  74. /// use serde::Serialize;
  75. ///
  76. /// // callback with large JSON argument
  77. /// #[derive(Serialize)]
  78. /// struct MyResponse {
  79. /// value: String
  80. /// }
  81. ///
  82. /// let cb = format_callback(
  83. /// CallbackFn(6789),
  84. /// &MyResponse { value: String::from_utf8(vec![b'X'; 10_240]).unwrap()
  85. /// }).expect("failed to serialize");
  86. ///
  87. /// assert!(cb.contains(r#"window["_6789"](JSON.parse('{"value":"XXXXXXXXX"#));
  88. /// ```
  89. pub fn format_callback<T: Serialize>(
  90. function_name: CallbackFn,
  91. arg: &T,
  92. ) -> crate::api::Result<String> {
  93. macro_rules! format_callback {
  94. ( $arg:expr ) => {
  95. format!(
  96. r#"
  97. if (window["_{fn}"]) {{
  98. window["_{fn}"]({arg})
  99. }} else {{
  100. console.warn("[TAURI] Couldn't find callback id {fn} in window. This happens when the app is reloaded while Rust is running an asynchronous operation.")
  101. }}
  102. "#,
  103. fn = function_name.0,
  104. arg = $arg
  105. )
  106. }
  107. }
  108. // get a raw &str representation of a serialized json value.
  109. let string = serde_json::to_string(arg)?;
  110. let raw = RawValue::from_string(string)?;
  111. // from here we know json.len() > 1 because an empty string is not a valid json value.
  112. let json = raw.get();
  113. let first = json.as_bytes()[0];
  114. #[cfg(debug_assertions)]
  115. if first == b'"' {
  116. debug_assert!(
  117. json.len() < MAX_JSON_STR_LEN,
  118. "passing a callback string larger than the max JavaScript literal string size"
  119. )
  120. }
  121. // only use JSON.parse('{arg}') for arrays and objects less than the limit
  122. // smaller literals do not benefit from being parsed from json
  123. Ok(
  124. if json.len() > MIN_JSON_PARSE_LEN && (first == b'{' || first == b'[') {
  125. let escaped = escape_json_parse(&raw);
  126. if escaped.len() < MAX_JSON_STR_LEN {
  127. format_callback!(escaped)
  128. } else {
  129. format_callback!(json)
  130. }
  131. } else {
  132. format_callback!(json)
  133. },
  134. )
  135. }
  136. /// Formats a Result type to its Promise response.
  137. /// Useful for Promises handling.
  138. /// If the Result `is_ok()`, the callback will be the `success_callback` function name and the argument will be the Ok value.
  139. /// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value.
  140. ///
  141. /// * `result` the Result to check
  142. /// * `success_callback` the function name of the Ok callback. Usually the `resolve` of the JS Promise.
  143. /// * `error_callback` the function name of the Err callback. Usually the `reject` of the JS Promise.
  144. ///
  145. /// Note that the callback strings are automatically generated by the `invoke` helper.
  146. ///
  147. /// # Examples
  148. /// ```
  149. /// use tauri::api::ipc::{CallbackFn, format_callback_result};
  150. /// let res: Result<u8, &str> = Ok(5);
  151. /// let cb = format_callback_result(res, CallbackFn(145), CallbackFn(0)).expect("failed to format");
  152. /// assert!(cb.contains(r#"window["_145"](5)"#));
  153. ///
  154. /// let res: Result<&str, &str> = Err("error message here");
  155. /// let cb = format_callback_result(res, CallbackFn(2), CallbackFn(1)).expect("failed to format");
  156. /// assert!(cb.contains(r#"window["_1"]("error message here")"#));
  157. /// ```
  158. // TODO: better example to explain
  159. pub fn format_callback_result<T: Serialize, E: Serialize>(
  160. result: Result<T, E>,
  161. success_callback: CallbackFn,
  162. error_callback: CallbackFn,
  163. ) -> crate::api::Result<String> {
  164. match result {
  165. Ok(res) => format_callback(success_callback, &res),
  166. Err(err) => format_callback(error_callback, &err),
  167. }
  168. }
  169. #[cfg(test)]
  170. mod test {
  171. use crate::api::ipc::*;
  172. use quickcheck::{Arbitrary, Gen};
  173. use quickcheck_macros::quickcheck;
  174. impl Arbitrary for CallbackFn {
  175. fn arbitrary(g: &mut Gen) -> CallbackFn {
  176. CallbackFn(usize::arbitrary(g))
  177. }
  178. }
  179. #[test]
  180. fn test_escape_json_parse() {
  181. let dangerous_json = RawValue::from_string(
  182. r#"{"test":"don\\🚀🐱‍👤\\'t forget to escape me!🚀🐱‍👤","te🚀🐱‍👤st2":"don't forget to escape me!","test3":"\\🚀🐱‍👤\\\\'''\\\\🚀🐱‍👤\\\\🚀🐱‍👤\\'''''"}"#.into()
  183. ).unwrap();
  184. let definitely_escaped_dangerous_json = format!(
  185. "JSON.parse('{}')",
  186. dangerous_json
  187. .get()
  188. .replace('\\', "\\\\")
  189. .replace('\'', "\\'")
  190. );
  191. let escape_single_quoted_json_test = escape_json_parse(&dangerous_json);
  192. let result = r#"JSON.parse('{"test":"don\\\\🚀🐱‍👤\\\\\'t forget to escape me!🚀🐱‍👤","te🚀🐱‍👤st2":"don\'t forget to escape me!","test3":"\\\\🚀🐱‍👤\\\\\\\\\'\'\'\\\\\\\\🚀🐱‍👤\\\\\\\\🚀🐱‍👤\\\\\'\'\'\'\'"}')"#;
  193. assert_eq!(definitely_escaped_dangerous_json, result);
  194. assert_eq!(escape_single_quoted_json_test, result);
  195. }
  196. // check abritrary strings in the format callback function
  197. #[quickcheck]
  198. fn qc_formating(f: CallbackFn, a: String) -> bool {
  199. // call format callback
  200. let fc = format_callback(f, &a).unwrap();
  201. fc.contains(&format!(
  202. r#"window["_{}"](JSON.parse('{}'))"#,
  203. f.0,
  204. serde_json::Value::String(a.clone()),
  205. )) || fc.contains(&format!(
  206. r#"window["_{}"]({})"#,
  207. f.0,
  208. serde_json::Value::String(a),
  209. ))
  210. }
  211. // check arbitrary strings in format_callback_result
  212. #[quickcheck]
  213. fn qc_format_res(result: Result<String, String>, c: CallbackFn, ec: CallbackFn) -> bool {
  214. let resp =
  215. format_callback_result(result.clone(), c, ec).expect("failed to format callback result");
  216. let (function, value) = match result {
  217. Ok(v) => (c, v),
  218. Err(e) => (ec, e),
  219. };
  220. resp.contains(&format!(
  221. r#"window["_{}"]({})"#,
  222. function.0,
  223. serde_json::Value::String(value),
  224. ))
  225. }
  226. }