1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

//! Types and functions related to Inter Procedure Call(IPC).
//!
//! This module includes utilities to send messages to the JS layer of the webview.

use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
pub use serialize_to_javascript::Options as SerializeOptions;
use serialize_to_javascript::Serialized;

/// The `Callback` type is the return value of the `transformCallback` JavaScript function.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
pub struct CallbackFn(pub usize);

/// 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).
///
/// [From MDN:](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length#description)
///
/// ECMAScript 2016 (ed. 7) established a maximum length of 2^53 - 1 elements. Previously, no maximum length was specified.
///
/// 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).
const MAX_JSON_STR_LEN: usize = usize::pow(2, 30) - 2;

/// Minimum size JSON needs to be in order to convert it to JSON.parse with [`format_json`].
// TODO: this number should be benchmarked and checked for optimal range, I set 10 KiB arbitrarily
// we don't want to lose the gained object parsing time to extra allocations preparing it
const MIN_JSON_PARSE_LEN: usize = 10_240;

/// Transforms & escapes a JSON value.
///
/// If it's an object or array, JSON.parse('{json}') is used, with the '{json}' string properly escaped.
/// The return value of this function can be safely used on [`eval`](crate::Window#method.eval) calls.
///
/// Single quotes chosen because double quotes are already used in JSON. With single quotes, we only
/// need to escape strings that include backslashes or single quotes. If we used double quotes, then
/// there would be no cases that a string doesn't need escaping.
///
/// The function takes a closure to handle the escaped string in order to avoid unnecessary allocations.
///
/// # Safety
///
/// The ability to safely escape JSON into a JSON.parse('{json}') relies entirely on 2 things.
///
/// 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.
///
/// # Examples
///
/// ```
/// use tauri::api::ipc::{serialize_js_with, SerializeOptions};
/// #[derive(serde::Serialize)]
/// struct Foo {
///   bar: String,
/// }
/// let foo = Foo { bar: "x".repeat(20_000).into() };
/// let value = serialize_js_with(&foo, SerializeOptions::default(), |v| format!("console.log({})", v)).unwrap();
/// assert_eq!(value, format!("console.log(JSON.parse('{{\"bar\":\"{}\"}}'))", foo.bar));
/// ```
pub fn serialize_js_with<T: Serialize, F: FnOnce(&str) -> String>(
  value: &T,
  options: SerializeOptions,
  cb: F,
) -> crate::api::Result<String> {
  // get a raw &str representation of a serialized json value.
  let string = serde_json::to_string(value)?;
  let raw = RawValue::from_string(string)?;

  // from here we know json.len() > 1 because an empty string is not a valid json value.
  let json = raw.get();
  let first = json.as_bytes()[0];

  #[cfg(debug_assertions)]
  if first == b'"' {
    assert!(
      json.len() < MAX_JSON_STR_LEN,
      "passing a string larger than the max JavaScript literal string size"
    )
  }

  let return_val = if json.len() > MIN_JSON_PARSE_LEN && (first == b'{' || first == b'[') {
    let serialized = Serialized::new(&raw, &options).into_string();
    // only use JSON.parse('{arg}') for arrays and objects less than the limit
    // smaller literals do not benefit from being parsed from json
    if serialized.len() < MAX_JSON_STR_LEN {
      cb(&serialized)
    } else {
      cb(json)
    }
  } else {
    cb(json)
  };

  Ok(return_val)
}

/// Transforms & escapes a JSON value.
///
/// This is a convenience function for [`serialize_js_with`], simply allocating the result to a String.
///
/// For usage in functions where performance is more important than code readability, see [`serialize_js_with`].
///
/// # Examples
/// ```rust,no_run
/// use tauri::{Manager, api::ipc::serialize_js};
/// use serde::Serialize;
///
/// #[derive(Serialize)]
/// struct Foo {
///   bar: String,
/// }
///
/// #[derive(Serialize)]
/// struct Bar {
///   baz: u32,
/// }
///
/// tauri::Builder::default()
///   .setup(|app| {
///     let window = app.get_window("main").unwrap();
///     window.eval(&format!(
///       "console.log({}, {})",
///       serialize_js(&Foo { bar: "bar".to_string() }).unwrap(),
///       serialize_js(&Bar { baz: 0 }).unwrap()),
///     )?;
///     Ok(())
///   });
/// ```
pub fn serialize_js<T: Serialize>(value: &T) -> crate::api::Result<String> {
  serialize_js_with(value, Default::default(), |v| v.into())
}

/// Formats a function name and 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).
///
/// # Examples
/// - With string literals:
/// ```
/// use tauri::api::ipc::{CallbackFn, format_callback};
/// // callback with a string argument
/// let cb = format_callback(CallbackFn(12345), &"the string response").unwrap();
/// assert!(cb.contains(r#"window["_12345"]("the string response")"#));
/// ```
///
/// - With types implement [`serde::Serialize`]:
/// ```
/// use tauri::api::ipc::{CallbackFn, format_callback};
/// use serde::Serialize;
///
/// // callback with large JSON argument
/// #[derive(Serialize)]
/// struct MyResponse {
///   value: String
/// }
///
/// let cb = format_callback(
///   CallbackFn(6789),
///   &MyResponse { value: String::from_utf8(vec![b'X'; 10_240]).unwrap()
/// }).expect("failed to serialize");
///
/// assert!(cb.contains(r#"window["_6789"](JSON.parse('{"value":"XXXXXXXXX"#));
/// ```
pub fn format_callback<T: Serialize>(
  function_name: CallbackFn,
  arg: &T,
) -> crate::api::Result<String> {
  serialize_js_with(arg, Default::default(), |arg| {
    format!(
      r#"
    if (window["_{fn}"]) {{
      window["_{fn}"]({arg})
    }} else {{
      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.")
    }}"#,
      fn = function_name.0,
      arg = arg
    )
  })
}

/// Formats a Result type 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.
///
/// * `result` the Result to check
/// * `success_callback` the function name of the Ok callback. Usually the `resolve` of the JS Promise.
/// * `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.
///
/// # Examples
/// ```
/// use tauri::api::ipc::{CallbackFn, format_callback_result};
/// let res: Result<u8, &str> = Ok(5);
/// let cb = format_callback_result(res, CallbackFn(145), CallbackFn(0)).expect("failed to format");
/// assert!(cb.contains(r#"window["_145"](5)"#));
///
/// let res: Result<&str, &str> = Err("error message here");
/// let cb = format_callback_result(res, CallbackFn(2), CallbackFn(1)).expect("failed to format");
/// assert!(cb.contains(r#"window["_1"]("error message here")"#));
/// ```
// TODO: better example to explain
pub fn format_callback_result<T: Serialize, E: Serialize>(
  result: Result<T, E>,
  success_callback: CallbackFn,
  error_callback: CallbackFn,
) -> crate::api::Result<String> {
  match result {
    Ok(res) => format_callback(success_callback, &res),
    Err(err) => format_callback(error_callback, &err),
  }
}

#[cfg(test)]
mod test {
  use crate::api::ipc::*;
  use quickcheck::{Arbitrary, Gen};
  use quickcheck_macros::quickcheck;

  impl Arbitrary for CallbackFn {
    fn arbitrary(g: &mut Gen) -> CallbackFn {
      CallbackFn(usize::arbitrary(g))
    }
  }

  #[test]
  fn test_serialize_js() {
    assert_eq!(serialize_js(&()).unwrap(), "null");
    assert_eq!(serialize_js(&5i32).unwrap(), "5");

    #[derive(serde::Serialize)]
    struct JsonObj {
      value: String,
    }

    let raw_str = "T".repeat(MIN_JSON_PARSE_LEN);
    assert_eq!(serialize_js(&raw_str).unwrap(), format!("\"{}\"", raw_str));

    assert_eq!(
      serialize_js(&JsonObj {
        value: raw_str.clone()
      })
      .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_formating(f: CallbackFn, a: String) -> bool {
    // call format callback
    let fc = format_callback(f, &a).unwrap();
    fc.contains(&format!(
      r#"window["_{}"](JSON.parse('{}'))"#,
      f.0,
      serde_json::Value::String(a.clone()),
    )) || fc.contains(&format!(
      r#"window["_{}"]({})"#,
      f.0,
      serde_json::Value::String(a),
    ))
  }

  // check arbitrary strings in format_callback_result
  #[quickcheck]
  fn qc_format_res(result: Result<String, String>, c: CallbackFn, ec: CallbackFn) -> bool {
    let resp =
      format_callback_result(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,
      serde_json::Value::String(value),
    ))
  }
}