Skip to content

Commit

Permalink
Add support for (de)serializing JsValues (#40)
Browse files Browse the repository at this point in the history
Co-authored-by: Ingvar Stepanyan <me@rreverser.com>
  • Loading branch information
jneem and RReverser authored Sep 4, 2023
1 parent 620cb3d commit 3a1d4e0
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 6 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ categories = ["development-tools::ffi", "wasm", "encoding"]
keywords = ["serde", "serialization", "javascript", "wasm", "webassembly"]

[dependencies]
serde = "^1.0"
serde = { version = "^1.0", features = ["derive"] }
js-sys = "^0.3"
wasm-bindgen = "0.2.83"

[dev-dependencies]
wasm-bindgen-test = "0.3.24"
serde = { version = "^1.0", features = ["derive"] }
serde_bytes = "0.11.1"
serde_json = "1.0.39"
maplit = "1.0.2"
bincode = "1.3.3"

[dev-dependencies.proptest]
version = "1.0"
Expand Down
40 changes: 38 additions & 2 deletions src/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ use js_sys::{Array, ArrayBuffer, JsString, Number, Object, Symbol, Uint8Array};
use serde::de::value::{MapDeserializer, SeqDeserializer};
use serde::de::{self, IntoDeserializer};
use std::convert::TryFrom;
use wasm_bindgen::convert::IntoWasmAbi;
use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt};

use super::{static_str_to_js, Error, ObjectExt, Result};
use crate::preserve::PRESERVED_VALUE_MAGIC;
use crate::{static_str_to_js, Error, ObjectExt, Result};

/// Provides [`de::SeqAccess`] from any JS iterator.
struct SeqAccess {
Expand Down Expand Up @@ -107,6 +109,37 @@ impl<'de> de::MapAccess<'de> for ObjectAccess {
}
}

enum PreservedValueAccess {
OnMagic(JsValue),
OnValue(JsValue),
Done,
}

impl<'de> de::SeqAccess<'de> for PreservedValueAccess {
type Error = Error;

fn next_element_seed<T: de::DeserializeSeed<'de>>(
&mut self,
seed: T,
) -> Result<Option<T::Value>> {
// Temporary replacement to avoid borrow checker issues when moving out `JsValue`.
let this = std::mem::replace(self, Self::Done);
match this {
Self::OnMagic(value) => {
*self = Self::OnValue(value);
seed.deserialize(str_deserializer(PRESERVED_VALUE_MAGIC))
.map(Some)
}
Self::OnValue(value) => seed
.deserialize(Deserializer {
value: JsValue::from(value.into_abi()),
})
.map(Some),
Self::Done => Ok(None),
}
}
}

/// Provides [`serde::de::EnumAccess`] from given JS values for the `tag` and the `payload`.
struct EnumAccess {
tag: Deserializer,
Expand Down Expand Up @@ -484,10 +517,13 @@ impl<'de> de::Deserializer<'de> for Deserializer {
/// Forwards to [`Self::deserialize_tuple`](#method.deserialize_tuple).
fn deserialize_tuple_struct<V: de::Visitor<'de>>(
self,
_name: &'static str,
name: &'static str,
len: usize,
visitor: V,
) -> Result<V::Value> {
if name == PRESERVED_VALUE_MAGIC {
return visitor.visit_seq(PreservedValueAccess::OnMagic(self.value));
}
self.deserialize_tuple(len, visitor)
}

Expand Down
107 changes: 107 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,110 @@ pub fn from_value<T: serde::de::DeserializeOwned>(value: JsValue) -> Result<T> {
pub fn to_value<T: serde::ser::Serialize + ?Sized>(value: &T) -> Result<JsValue> {
value.serialize(&Serializer::new())
}

/// Serialization and deserialization functions that pass JavaScript objects through unchanged.
///
/// This module is compatible with the `serde(with)` annotation, so for example if you create
/// the struct
///
/// ```rust
/// #[derive(serde::Serialize)]
/// struct MyStruct {
/// int_field: i32,
/// #[serde(with = "serde_wasm_bindgen::preserve")]
/// js_field: js_sys::Int8Array,
/// }
///
/// let s = MyStruct {
/// int_field: 5,
/// js_field: js_sys::Int8Array::new_with_length(1000),
/// };
/// ```
///
/// then `serde_wasm_bindgen::to_value(&s)`
/// will return a JsValue representing an object with two fields (`int_field` and `js_field`), where
/// `js_field` will be an `Int8Array` pointing to the same underlying JavaScript object as `s.js_field` does.
pub mod preserve {
use serde::{de::Error, Deserialize, Serialize};
use wasm_bindgen::{
convert::{FromWasmAbi, IntoWasmAbi},
JsCast, JsValue,
};

// Some arbitrary string that no one will collide with unless they try.
pub(crate) const PRESERVED_VALUE_MAGIC: &str = "1fc430ca-5b7f-4295-92de-33cf2b145d38";

struct Magic;

impl<'de> serde::de::Deserialize<'de> for Magic {
fn deserialize<D: serde::de::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
struct Visitor;

impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = Magic;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("serde-wasm-bindgen's magic string")
}

fn visit_str<E: Error>(self, s: &str) -> Result<Self::Value, E> {
if s == PRESERVED_VALUE_MAGIC {
Ok(Magic)
} else {
Err(E::invalid_value(serde::de::Unexpected::Str(s), &self))
}
}
}

de.deserialize_str(Visitor)
}
}

#[derive(Serialize)]
#[serde(rename = "1fc430ca-5b7f-4295-92de-33cf2b145d38")]
struct PreservedValueSerWrapper(u32);

// Intentionally asymmetrical wrapper to ensure that only serde-wasm-bindgen preserves roundtrip.
#[derive(Deserialize)]
#[serde(rename = "1fc430ca-5b7f-4295-92de-33cf2b145d38")]
struct PreservedValueDeWrapper(Magic, u32);

/// Serialize any `JsCast` value.
///
/// When used with the `Serializer` in `serde_wasm_bindgen`, this serializes the value by
/// passing it through as a `JsValue`.
///
/// This function is compatible with the `serde(serialize_with)` derive annotation.
pub fn serialize<S: serde::Serializer, T: JsCast>(val: &T, ser: S) -> Result<S::Ok, S::Error> {
// It's responsibility of serde-wasm-bindgen's Serializer to clone the value.
// For all other serializers, using reference instead of cloning here will ensure that we don't
// create accidental leaks.
PreservedValueSerWrapper(val.as_ref().into_abi()).serialize(ser)
}

/// Deserialize any `JsCast` value.
///
/// When used with the `Derializer` in `serde_wasm_bindgen`, this serializes the value by
/// passing it through as a `JsValue` and casting it.
///
/// This function is compatible with the `serde(deserialize_with)` derive annotation.
pub fn deserialize<'de, D: serde::Deserializer<'de>, T: JsCast>(de: D) -> Result<T, D::Error> {
let wrap = PreservedValueDeWrapper::deserialize(de)?;
// When used with our deserializer this unsafe is correct, because the
// deserializer just converted a JsValue into_abi.
//
// Other deserializers are unlikely to end up here, thanks
// to the asymmetry between PreservedValueSerWrapper and
// PreservedValueDeWrapper. Even if some other deserializer ends up
// here, this may be incorrect but it shouldn't be UB because JsValues
// are represented using indices into a JS-side (i.e. bounds-checked)
// array.
let val: JsValue = unsafe { FromWasmAbi::from_abi(wrap.1) };
val.dyn_into().map_err(|e| {
D::Error::custom(format_args!(
"incompatible JS value {e:?} for type {}",
std::any::type_name::<T>()
))
})
}
}
13 changes: 11 additions & 2 deletions src/ser.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use js_sys::{Array, JsString, Map, Number, Object, Uint8Array};
use serde::ser::{self, Error as _, Serialize};
use wasm_bindgen::convert::RefFromWasmAbi;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;

use super::{static_str_to_js, Error, ObjectExt};
use crate::preserve::PRESERVED_VALUE_MAGIC;
use crate::{static_str_to_js, Error, ObjectExt};

type Result<T = JsValue> = super::Result<T>;

Expand Down Expand Up @@ -404,9 +406,16 @@ impl<'s> ser::Serializer for &'s Serializer {

fn serialize_newtype_struct<T: ?Sized + Serialize>(
self,
_name: &'static str,
name: &'static str,
value: &T,
) -> Result {
if name == PRESERVED_VALUE_MAGIC {
let abi = value.serialize(self)?.unchecked_into_f64() as u32;
// `PreservedValueSerWrapper` gives us ABI of a reference to a `JsValue` that is
// guaranteed to be alive only during this call.
// We must clone it before giving away the value to the caller.
return Ok(unsafe { JsValue::ref_from_abi(abi) }.as_ref().clone());
}
value.serialize(self)
}

Expand Down
36 changes: 36 additions & 0 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,42 @@ fn enums() {
}
}

#[wasm_bindgen_test]
fn preserved_value() {
#[derive(serde::Deserialize, serde::Serialize, PartialEq, Clone, Debug)]
#[serde(bound = "T: JsCast")]
struct PreservedValue<T: JsCast>(#[serde(with = "serde_wasm_bindgen::preserve")] T);

test_via_into(PreservedValue(JsValue::from_f64(42.0)), 42);
test_via_into(PreservedValue(JsValue::from_str("hello")), "hello");

let res: PreservedValue<JsValue> = from_value(JsValue::from_f64(42.0)).unwrap();
assert_eq!(res.0.as_f64(), Some(42.0));

// Check that object identity is preserved.
let big_array = js_sys::Int8Array::new_with_length(64);
let val = PreservedValue(big_array);
let res = to_value(&val).unwrap();
assert_eq!(res, JsValue::from(val.0));

// The JsCasts are checked on deserialization.
let bool = js_sys::Boolean::from(true);
let serialized = to_value(&PreservedValue(bool)).unwrap();
let res: Result<PreservedValue<Number>, _> = from_value(serialized);
assert_eq!(
res.unwrap_err().to_string(),
Error::custom("incompatible JS value JsValue(true) for type js_sys::Number").to_string()
);

// serde_json must fail to round-trip our special wrapper
let s = serde_json::to_string(&PreservedValue(JsValue::from_f64(42.0))).unwrap();
serde_json::from_str::<PreservedValue<JsValue>>(&s).unwrap_err();

// bincode must fail to round-trip our special wrapper
let s = bincode::serialize(&PreservedValue(JsValue::from_f64(42.0))).unwrap();
bincode::deserialize::<PreservedValue<JsValue>>(&s).unwrap_err();
}

#[wasm_bindgen_test]
fn structs() {
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
Expand Down

0 comments on commit 3a1d4e0

Please sign in to comment.