-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
/
ipc.rs
249 lines (226 loc) · 9.05 KB
/
ipc.rs
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
// 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;
/// 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 [`escape_json_parse`].
// 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 String -> JSON.parse('{json}')
///
/// 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.
///
/// # 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.
fn escape_json_parse(json: &RawValue) -> String {
let json = json.get();
// 14 chars in JSON.parse('')
// todo: should we increase the 14 by x to allow x amount of escapes before another allocation?
let mut s = String::with_capacity(json.len() + 14);
s.push_str("JSON.parse('");
// insert a backslash before any backslash or single quote characters.
let mut last = 0;
for (idx, _) in json.match_indices(|c| c == '\\' || c == '\'') {
s.push_str(&json[last..idx]);
s.push('\\');
last = idx;
}
// finish appending the trailing characters that don't need escaping
s.push_str(&json[last..]);
s.push_str("')");
s
}
/// 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::format_callback;
/// // callback with a string argument
/// let cb = format_callback(12345, &"the string response").unwrap();
/// assert!(cb.contains(r#"window["12345"]("the string response")"#));
/// ```
///
/// - With types implement [`serde::Serialize`]:
/// ```
/// use tauri::api::ipc::format_callback;
/// use serde::Serialize;
///
/// // callback with large JSON argument
/// #[derive(Serialize)]
/// struct MyResponse {
/// value: String
/// }
///
/// let cb = format_callback(
/// 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> {
macro_rules! format_callback {
( $arg:expr ) => {
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
)
}
}
// get a raw &str representation of a serialized json value.
let string = serde_json::to_string(arg)?;
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'"' {
debug_assert!(
json.len() < MAX_JSON_STR_LEN,
"passing a callback string larger than the max JavaScript literal string size"
)
}
// only use JSON.parse('{arg}') for arrays and objects less than the limit
// smaller literals do not benefit from being parsed from json
Ok(
if json.len() > MIN_JSON_PARSE_LEN && (first == b'{' || first == b'[') {
let escaped = escape_json_parse(&raw);
if escaped.len() < MAX_JSON_STR_LEN {
format_callback!(escaped)
} else {
format_callback!(json)
}
} else {
format_callback!(json)
},
)
}
/// 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::format_callback_result;
/// let res: Result<u8, &str> = Ok(5);
/// let cb = format_callback_result(res, "success_cb", "error_cb").expect("failed to format");
/// assert!(cb.contains(r#"window["success_cb"](5)"#));
///
/// let res: Result<&str, &str> = Err("error message here");
/// let cb = format_callback_result(res, "success_cb", "error_cb").expect("failed to format");
/// assert!(cb.contains(r#"window["error_cb"]("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_escape_json_parse() {
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 = escape_json_parse(&dangerous_json);
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 abritrary 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),
))
}
}