use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
pub use serialize_to_javascript::Options as SerializeOptions;
use serialize_to_javascript::Serialized;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
pub struct CallbackFn(pub usize);
const MAX_JSON_STR_LEN: usize = usize::pow(2, 30) - 2;
const MIN_JSON_PARSE_LEN: usize = 10_240;
pub fn serialize_js_with<T: Serialize, F: FnOnce(&str) -> String>(
value: &T,
options: SerializeOptions,
cb: F,
) -> crate::api::Result<String> {
let string = serde_json::to_string(value)?;
let raw = RawValue::from_string(string)?;
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();
if serialized.len() < MAX_JSON_STR_LEN {
cb(&serialized)
} else {
cb(json)
}
} else {
cb(json)
};
Ok(return_val)
}
pub fn serialize_js<T: Serialize>(value: &T) -> crate::api::Result<String> {
serialize_js_with(value, Default::default(), |v| v.into())
}
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
)
})
}
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);
}
#[quickcheck]
fn qc_formating(f: CallbackFn, a: String) -> bool {
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),
))
}
#[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),
))
}
}