#![allow(clippy::tabs_in_doc_comments)]
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('[Millennium] 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 quickcheck::{Arbitrary, Gen};
use quickcheck_macros::quickcheck;
use crate::api::ipc::*;
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),))
}
}