tauri/ipc/
format_callback.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5use serde::Serialize;
6use serde_json::value::RawValue;
7use serialize_to_javascript::Serialized;
8
9use super::CallbackFn;
10
11/// 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).
12///
13/// [From MDN:](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length#description)
14///
15/// ECMAScript 2016 (ed. 7) established a maximum length of 2^53 - 1 elements. Previously, no maximum length was specified.
16///
17/// 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).
18const MAX_JSON_STR_LEN: usize = usize::pow(2, 30) - 2;
19
20/// Minimum size JSON needs to be in order to convert it to JSON.parse with [`format_json`].
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
23const MIN_JSON_PARSE_LEN: usize = 10_240;
24
25/// Transforms & escapes a JSON value.
26///
27/// If it's an object or array, JSON.parse('{json}') is used, with the '{json}' string properly escaped.
28/// The return value of this function can be safely used on [`eval`](crate::Window#method.eval) calls.
29///
30/// Single quotes chosen because double quotes are already used in JSON. With single quotes, we only
31/// need to escape strings that include backslashes or single quotes. If we used double quotes, then
32/// there would be no cases that a string doesn't need escaping.
33///
34/// The function takes a closure to handle the escaped string in order to avoid unnecessary allocations.
35///
36/// # Safety
37///
38/// The ability to safely escape JSON into a JSON.parse('{json}') relies entirely on 2 things.
39///
40/// 1. `serde_json`'s ability to correctly escape and format json into a string.
41/// 2. JavaScript engines not accepting anything except another unescaped, literal single quote
42///    character to end a string that was opened with it.
43fn serialize_js_with<F: FnOnce(&str) -> String>(
44  json_string: String,
45  options: serialize_to_javascript::Options,
46  cb: F,
47) -> crate::Result<String> {
48  // get a raw &str representation of a serialized json value.
49
50  let raw = RawValue::from_string(json_string)?;
51
52  // from here we know json.len() > 1 because an empty string is not a valid json value.
53  let json = raw.get();
54  let first = json.as_bytes()[0];
55
56  #[cfg(debug_assertions)]
57  if first == b'"' {
58    assert!(
59      json.len() < MAX_JSON_STR_LEN,
60      "passing a string larger than the max JavaScript literal string size"
61    )
62  }
63
64  let return_val = if json.len() > MIN_JSON_PARSE_LEN && (first == b'{' || first == b'[') {
65    let serialized = Serialized::new(&raw, &options).into_string();
66    // only use JSON.parse('{arg}') for arrays and objects less than the limit
67    // smaller literals do not benefit from being parsed from json
68    if serialized.len() < MAX_JSON_STR_LEN {
69      cb(&serialized)
70    } else {
71      cb(json)
72    }
73  } else {
74    cb(json)
75  };
76
77  Ok(return_val)
78}
79
80/// Formats a function name and a serializable argument to be evaluated as callback.
81///
82/// See [`format_raw`] for more information.
83pub fn format<T: Serialize>(function_name: CallbackFn, arg: &T) -> crate::Result<String> {
84  format_raw(function_name, serde_json::to_string(arg)?)
85}
86
87/// Formats a function name and a raw JSON string argument to be evaluated as callback.
88///
89/// This will serialize primitive JSON types (e.g. booleans, strings, numbers, etc.) as JavaScript literals,
90/// but will serialize arrays and objects whose serialized JSON string is smaller than 1 GB and larger
91/// than 10 KiB with `JSON.parse('...')`.
92/// See [json-parse-benchmark](https://github.com/GoogleChromeLabs/json-parse-benchmark).
93pub fn format_raw(function_name: CallbackFn, json_string: String) -> crate::Result<String> {
94  let callback_id = function_name.0;
95  serialize_js_with(json_string, Default::default(), |arg| {
96    format_raw_js(callback_id, arg)
97  })
98}
99
100/// Formats a function name and a JavaScript string argument to be evaluated as callback.
101pub fn format_raw_js(callback_id: u32, js: impl AsRef<str>) -> String {
102  fn format_inner(callback_id: u32, js: &str) -> String {
103    format!("window.__TAURI_INTERNALS__.runCallback({callback_id}, {js})")
104  }
105  format_inner(callback_id, js.as_ref())
106}
107
108/// Formats a serializable Result type to its Promise response.
109///
110/// See [`format_result_raw`] for more information.
111pub fn format_result<T: Serialize, E: Serialize>(
112  result: Result<T, E>,
113  success_callback: CallbackFn,
114  error_callback: CallbackFn,
115) -> crate::Result<String> {
116  match result {
117    Ok(res) => format(success_callback, &res),
118    Err(err) => format(error_callback, &err),
119  }
120}
121
122/// Formats a Result type of raw JSON strings to its Promise response.
123/// Useful for Promises handling.
124/// If the Result `is_ok()`, the callback will be the `success_callback` function name and the argument will be the Ok value.
125/// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value.
126///
127/// * `result` the Result to check
128/// * `success_callback` the function name of the Ok callback. Usually the `resolve` of the JS Promise.
129/// * `error_callback` the function name of the Err callback. Usually the `reject` of the JS Promise.
130///
131/// Note that the callback strings are automatically generated by the `invoke` helper.
132pub fn format_result_raw(
133  raw_result: Result<String, String>,
134  success_callback: CallbackFn,
135  error_callback: CallbackFn,
136) -> crate::Result<String> {
137  match raw_result {
138    Ok(res) => format_raw(success_callback, res),
139    Err(err) => format_raw(error_callback, err),
140  }
141}
142
143#[cfg(test)]
144mod test {
145  use super::*;
146  use quickcheck::{Arbitrary, Gen};
147  use quickcheck_macros::quickcheck;
148
149  impl Arbitrary for CallbackFn {
150    fn arbitrary(g: &mut Gen) -> CallbackFn {
151      CallbackFn(u32::arbitrary(g))
152    }
153  }
154
155  #[derive(Debug, Clone)]
156  struct JsonStr(String);
157
158  impl Arbitrary for JsonStr {
159    fn arbitrary(g: &mut Gen) -> Self {
160      if bool::arbitrary(g) {
161        Self(format!(
162          "{{ {}: {} }}",
163          serde_json::to_string(&String::arbitrary(g)).unwrap(),
164          serde_json::to_string(&String::arbitrary(g)).unwrap()
165        ))
166      } else {
167        Self(serde_json::to_string(&String::arbitrary(g)).unwrap())
168      }
169    }
170  }
171
172  fn serialize_js<T: Serialize>(value: &T) -> crate::Result<String> {
173    serialize_js_with(serde_json::to_string(value)?, Default::default(), |v| {
174      v.into()
175    })
176  }
177
178  fn serialize_js_raw(value: impl Into<String>) -> crate::Result<String> {
179    serialize_js_with(value.into(), Default::default(), |v| v.into())
180  }
181
182  #[test]
183  fn test_serialize_js() {
184    assert_eq!(serialize_js(&()).unwrap(), "null");
185    assert_eq!(serialize_js(&5i32).unwrap(), "5");
186
187    #[derive(serde::Serialize)]
188    struct JsonObj {
189      value: String,
190    }
191
192    let raw_str = "T".repeat(MIN_JSON_PARSE_LEN);
193    assert_eq!(serialize_js(&raw_str).unwrap(), format!("\"{raw_str}\""));
194
195    assert_eq!(
196      serialize_js(&JsonObj {
197        value: raw_str.clone()
198      })
199      .unwrap(),
200      format!("JSON.parse('{{\"value\":\"{raw_str}\"}}')")
201    );
202
203    assert_eq!(
204      serialize_js(&JsonObj {
205        value: format!("\"{raw_str}\"")
206      })
207      .unwrap(),
208      format!("JSON.parse('{{\"value\":\"\\\\\"{raw_str}\\\\\"\"}}')")
209    );
210
211    let dangerous_json = RawValue::from_string(
212      r#"{"test":"don\\πŸš€πŸ±β€πŸ‘€\\'t forget to escape me!πŸš€πŸ±β€πŸ‘€","teπŸš€πŸ±β€πŸ‘€st2":"don't forget to escape me!","test3":"\\πŸš€πŸ±β€πŸ‘€\\\\'''\\\\πŸš€πŸ±β€πŸ‘€\\\\πŸš€πŸ±β€πŸ‘€\\'''''"}"#.into()
213    ).unwrap();
214
215    let definitely_escaped_dangerous_json = format!(
216      "JSON.parse('{}')",
217      dangerous_json
218        .get()
219        .replace('\\', "\\\\")
220        .replace('\'', "\\'")
221    );
222    let escape_single_quoted_json_test =
223      serialize_to_javascript::Serialized::new(&dangerous_json, &Default::default()).into_string();
224
225    let result = r#"JSON.parse('{"test":"don\\\\πŸš€πŸ±β€πŸ‘€\\\\\'t forget to escape me!πŸš€πŸ±β€πŸ‘€","teπŸš€πŸ±β€πŸ‘€st2":"don\'t forget to escape me!","test3":"\\\\πŸš€πŸ±β€πŸ‘€\\\\\\\\\'\'\'\\\\\\\\πŸš€πŸ±β€πŸ‘€\\\\\\\\πŸš€πŸ±β€πŸ‘€\\\\\'\'\'\'\'"}')"#;
226    assert_eq!(definitely_escaped_dangerous_json, result);
227    assert_eq!(escape_single_quoted_json_test, result);
228  }
229
230  // check arbitrary strings in the format callback function
231  #[quickcheck]
232  fn qc_formatting(f: CallbackFn, a: String) -> bool {
233    // call format callback
234    let fc = format(f, &a).unwrap();
235    fc.contains(&format!(
236      "window.__TAURI_INTERNALS__.runCallback({}, JSON.parse('{}'))",
237      f.0,
238      serde_json::Value::String(a.clone()),
239    )) || fc.contains(&format!(
240      r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#,
241      f.0,
242      serde_json::Value::String(a),
243    ))
244  }
245
246  // check arbitrary strings in format_result
247  #[quickcheck]
248  fn qc_format_res(result: Result<String, String>, c: CallbackFn, ec: CallbackFn) -> bool {
249    let resp = format_result(result.clone(), c, ec).expect("failed to format callback result");
250    let (function, value) = match result {
251      Ok(v) => (c, v),
252      Err(e) => (ec, e),
253    };
254
255    resp.contains(&format!(
256      r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#,
257      function.0,
258      serde_json::Value::String(value),
259    ))
260  }
261
262  #[test]
263  fn test_serialize_js_raw() {
264    assert_eq!(serialize_js_raw("null").unwrap(), "null");
265    assert_eq!(serialize_js_raw("5").unwrap(), "5");
266    assert_eq!(
267      serialize_js_raw("{ \"x\": [1, 2, 3] }").unwrap(),
268      "{ \"x\": [1, 2, 3] }"
269    );
270
271    #[derive(serde::Serialize)]
272    struct JsonObj {
273      value: String,
274    }
275
276    let raw_str = "T".repeat(MIN_JSON_PARSE_LEN);
277    assert_eq!(
278      serialize_js_raw(format!("\"{raw_str}\"")).unwrap(),
279      format!("\"{raw_str}\"")
280    );
281
282    assert_eq!(
283      serialize_js_raw(format!("{{\"value\":\"{raw_str}\"}}")).unwrap(),
284      format!("JSON.parse('{{\"value\":\"{raw_str}\"}}')")
285    );
286
287    assert_eq!(
288      serialize_js(&JsonObj {
289        value: format!("\"{raw_str}\"")
290      })
291      .unwrap(),
292      format!("JSON.parse('{{\"value\":\"\\\\\"{raw_str}\\\\\"\"}}')")
293    );
294
295    let dangerous_json = RawValue::from_string(
296      r#"{"test":"don\\πŸš€πŸ±β€πŸ‘€\\'t forget to escape me!πŸš€πŸ±β€πŸ‘€","teπŸš€πŸ±β€πŸ‘€st2":"don't forget to escape me!","test3":"\\πŸš€πŸ±β€πŸ‘€\\\\'''\\\\πŸš€πŸ±β€πŸ‘€\\\\πŸš€πŸ±β€πŸ‘€\\'''''"}"#.into()
297    ).unwrap();
298
299    let definitely_escaped_dangerous_json = format!(
300      "JSON.parse('{}')",
301      dangerous_json
302        .get()
303        .replace('\\', "\\\\")
304        .replace('\'', "\\'")
305    );
306    let escape_single_quoted_json_test =
307      serialize_to_javascript::Serialized::new(&dangerous_json, &Default::default()).into_string();
308
309    let result = r#"JSON.parse('{"test":"don\\\\πŸš€πŸ±β€πŸ‘€\\\\\'t forget to escape me!πŸš€πŸ±β€πŸ‘€","teπŸš€πŸ±β€πŸ‘€st2":"don\'t forget to escape me!","test3":"\\\\πŸš€πŸ±β€πŸ‘€\\\\\\\\\'\'\'\\\\\\\\πŸš€πŸ±β€πŸ‘€\\\\\\\\πŸš€πŸ±β€πŸ‘€\\\\\'\'\'\'\'"}')"#;
310    assert_eq!(definitely_escaped_dangerous_json, result);
311    assert_eq!(escape_single_quoted_json_test, result);
312  }
313
314  // check arbitrary strings in the format callback function
315  #[quickcheck]
316  fn qc_formatting_raw(f: CallbackFn, a: JsonStr) -> bool {
317    let a = a.0;
318    // call format callback
319    let fc = format_raw(f, a.clone()).unwrap();
320    fc.contains(&format!(
321      r#"window.__TAURI_INTERNALS__.runCallback({}, JSON.parse('{}'))"#,
322      f.0, a
323    )) || fc.contains(&format!(
324      r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#,
325      f.0, a
326    ))
327  }
328
329  // check arbitrary strings in format_result
330  #[quickcheck]
331  fn qc_format_raw_res(result: Result<JsonStr, JsonStr>, c: CallbackFn, ec: CallbackFn) -> bool {
332    let result = result.map(|v| v.0).map_err(|e| e.0);
333    let resp = format_result_raw(result.clone(), c, ec).expect("failed to format callback result");
334    let (function, value) = match result {
335      Ok(v) => (c, v),
336      Err(e) => (ec, e),
337    };
338
339    resp.contains(&format!(
340      r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#,
341      function.0, value
342    ))
343  }
344}