v8_valueserializer 0.1.1

A Rust implementation of V8's ValueSerializer and ValueDeserializer
Documentation
use v8::Global;
use v8::Local;
use v8::Value;
use v8::ValueDeserializerHelper;
use v8::ValueSerializerHelper;
use v8_valueserializer::Heap;

static ONCE: std::sync::Once = std::sync::Once::new();

fn init_v8() {
  v8::V8::initialize_platform(v8::new_default_platform(0, false).make_shared());
  v8::V8::initialize();
}

#[derive(Debug)]
pub struct JsError {
  #[allow(dead_code)]
  message: String,
}

pub struct Isolate {
  isolate: v8::OwnedIsolate,
  context: v8::Global<v8::Context>,
}

impl Default for Isolate {
  fn default() -> Isolate {
    ONCE.call_once(init_v8);
    let mut isolate = v8::Isolate::new(Default::default());
    let mut scope = v8::HandleScope::new(&mut isolate);
    let context = v8::Context::new(&mut scope);
    let context = v8::Global::new(&mut scope, context);
    drop(scope);
    Isolate { isolate, context }
  }
}

impl Isolate {
  #[allow(dead_code)]
  pub fn deserialize(
    &mut self,
    serialized: &[u8],
  ) -> Result<Global<Value>, JsError> {
    let scope = &mut v8::HandleScope::new(&mut self.isolate);
    let context = v8::Local::new(scope, &self.context);
    let scope = &mut v8::ContextScope::new(scope, context);

    let tc = &mut v8::TryCatch::new(scope);
    let mut deserializer = v8::ValueDeserializer::new(
      tc,
      Box::new(ValueDeserializerImpl),
      serialized,
    );

    let Some(true) = deserializer.read_header(context) else {
      let message = tc.message().unwrap();
      let message = message.get(tc).to_rust_string_lossy(tc);
      return Err(JsError { message });
    };

    let Some(value) = deserializer.read_value(context) else {
      let message = tc.message().unwrap();
      let message = message.get(tc).to_rust_string_lossy(tc);
      return Err(JsError { message });
    };

    let global = Global::new(tc, value);

    Ok(global)
  }

  #[allow(dead_code)]
  pub fn eval(&mut self, code: &str) -> Result<Global<Value>, JsError> {
    let scope = &mut v8::HandleScope::new(&mut self.isolate);
    let context = v8::Local::new(scope, &self.context);
    let scope = &mut v8::ContextScope::new(scope, context);

    let tc = &mut v8::TryCatch::new(scope);

    let Some(source) =
      v8::String::new_from_utf8(tc, code.as_bytes(), v8::NewStringType::Normal)
    else {
      let message = tc.message().unwrap();
      let message = message.get(tc).to_rust_string_lossy(tc);
      return Err(JsError { message });
    };

    let Some(script) = v8::Script::compile(tc, source, None) else {
      let message = tc.message().unwrap();
      let message = message.get(tc).to_rust_string_lossy(tc);
      return Err(JsError { message });
    };

    let Some(value) = script.run(tc) else {
      let message = tc.message().unwrap();
      let message = message.get(tc).to_rust_string_lossy(tc);
      return Err(JsError { message });
    };

    let global = Global::new(tc, value);
    Ok(global)
  }

  #[allow(dead_code)]
  pub fn serialize_value(
    &mut self,
    value: Global<Value>,
  ) -> Result<Vec<u8>, JsError> {
    let scope = &mut v8::HandleScope::new(&mut self.isolate);
    let context = v8::Local::new(scope, &self.context);
    let scope = &mut v8::ContextScope::new(scope, context);

    let tc = &mut v8::TryCatch::new(scope);
    let mut serializer =
      v8::ValueSerializer::new(tc, Box::new(ValueSerializerImpl));

    let value = Local::new(tc, &value);

    serializer.write_header();
    let Some(true) = serializer.write_value(context, value) else {
      let message = tc.message().unwrap();
      let message = message.get(tc).to_rust_string_lossy(tc);
      return Err(JsError { message });
    };

    let buf = serializer.release();
    Ok(buf)
  }
}

struct ValueDeserializerImpl;

impl v8::ValueDeserializerImpl for ValueDeserializerImpl {
  fn read_host_object<'s>(
    &mut self,
    scope: &mut v8::HandleScope<'s>,
    _value_deserializer: &mut dyn v8::ValueDeserializerHelper,
  ) -> Option<v8::Local<'s, v8::Object>> {
    let msg = v8::String::new(
      scope,
      "Deno deserializer: read_host_object not implemented",
    )
    .unwrap();
    let exc = v8::Exception::error(scope, msg);
    scope.throw_exception(exc);
    None
  }

  fn get_shared_array_buffer_from_id<'s>(
    &mut self,
    scope: &mut v8::HandleScope<'s>,
    _transfer_id: u32,
  ) -> Option<v8::Local<'s, v8::SharedArrayBuffer>> {
    let msg = v8::String::new(
      scope,
      "Deno deserializer: get_shared_array_buffer_from_id not implemented",
    )
    .unwrap();
    let exc = v8::Exception::error(scope, msg);
    scope.throw_exception(exc);
    None
  }

  fn get_wasm_module_from_id<'s>(
    &mut self,
    scope: &mut v8::HandleScope<'s>,
    _clone_id: u32,
  ) -> Option<v8::Local<'s, v8::WasmModuleObject>> {
    let msg = v8::String::new(
      scope,
      "Deno deserializer: get_wasm_module_from_id not implemented",
    )
    .unwrap();
    let exc = v8::Exception::error(scope, msg);
    scope.throw_exception(exc);
    None
  }
}

struct ValueSerializerImpl;

impl v8::ValueSerializerImpl for ValueSerializerImpl {
  fn throw_data_clone_error<'s>(
    &mut self,
    scope: &mut v8::HandleScope<'s>,
    message: v8::Local<'s, v8::String>,
  ) {
    let exc = v8::Exception::error(scope, message);
    scope.throw_exception(exc);
  }
}

pub struct Assert {
  pub value: v8_valueserializer::Value,
  pub heap: Heap,
}

impl std::fmt::Debug for Assert {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    writeln!(f, "=== VALUE ===")?;
    writeln!(f, "{:?}", self.value)?;
    writeln!(f)?;
    writeln!(f, "=== HEAP ===")?;
    std::fmt::Debug::fmt(&self.heap, f)?;
    writeln!(f)?;
    Ok(())
  }
}

#[macro_export]
macro_rules! serde_test {
  ($name:ident $code:expr) => {
    #[test]
    fn $name() {
      let mut isolate = $crate::util::Isolate::default();
      let code = if $code.starts_with("{") {
        format!("({})", $code)
      } else {
        $code.to_string()
      };

      let input_js_value = isolate.eval(code.as_str()).expect("eval failed");
      let input_bytes = isolate
        .serialize_value(input_js_value)
        .expect("serialize_value failed");
      println!("injs_bytes {:?}", input_bytes);
      let de = v8_valueserializer::ValueDeserializer::default();

      let (value, heap) = de.read(&input_bytes).expect("original parse_v8 failed");
      let assert = $crate::util::Assert {
        value,
        heap,
      };
      insta::with_settings!({
        description => format!("=== SOURCE ===\n{}", $code),
        omit_expression => true,
      }, {
        insta::assert_debug_snapshot!(concat!("de_", stringify!($name)), assert);
      });

      let ser = v8_valueserializer::ValueSerializer::default();
      let rs_ser_bytes = ser.finish(&assert.heap, &assert.value).expect("serialize failed");
      println!("rsserbytes {:?}", rs_ser_bytes);

      let rs_roundtripped_value = isolate
        .deserialize(&rs_ser_bytes)
        .expect("rs deserialize failed");
      let rs_roundtripped_bytes = isolate
        .serialize_value(rs_roundtripped_value)
        .expect("rs serialize_value failed");
      println!("rsrt_bytes {:?}", rs_roundtripped_bytes);

      let de = v8_valueserializer::ValueDeserializer::default();
      let (rs_rt_value, rs_rt_heap) = de.read(&rs_roundtripped_bytes).expect("parse_v8 failed");

      if !v8_valueserializer::value_eq((&assert.value, &assert.heap), (&rs_rt_value, &rs_rt_heap)) {
        println!("=== EXPECTED ===");
        println!("{:?}", assert);

        let rt_assert = $crate::util::Assert {
          value: rs_rt_value,
          heap: rs_rt_heap,
        };
        println!("=== ROUNDTRIPPED ===");
        println!("{:?}", rt_assert);

        panic!("serialized roundtrip failed");
      }

      let display = v8_valueserializer::display(&assert.heap, &assert.value, v8_valueserializer::DisplayOptions {
        format: v8_valueserializer::DisplayFormat::Repl,
      });
      insta::with_settings!({
        description => format!("=== SOURCE ===\n{}", $code),
        omit_expression => true,
      }, {
        insta::assert_snapshot!(concat!("display_", stringify!($name)), display);
      });

      let eval = v8_valueserializer::display(&assert.heap, &assert.value, v8_valueserializer::DisplayOptions {
        format: v8_valueserializer::DisplayFormat::Eval,
      });
      let display_rt_js_value = isolate.eval(eval.as_str()).expect("eval failed");
      let display_rt_bytes = isolate
        .serialize_value(display_rt_js_value)
        .expect("serialize_value failed");
      println!("display_rt_bytes {:?}", display_rt_bytes);

      let de = v8_valueserializer::ValueDeserializer::default();
      let (display_rt_value, display_rt_heap) = de.read(&display_rt_bytes).expect("parse_v8 failed");

      if !v8_valueserializer::value_eq((&assert.value, &assert.heap), (&display_rt_value, &display_rt_heap)) {
        println!("=== EXPECTED ===");
        println!("{:?}", assert);

        let rt_assert = $crate::util::Assert {
          value: display_rt_value,
          heap: display_rt_heap,
        };
        println!("=== ROUNDTRIPPED ===");
        println!("{:?}", rt_assert);

        panic!("display roundtrip failed");
      }

    }
  };
}

#[macro_export]
macro_rules! display_test {
  ($name:ident $code:expr) => {
    #[test]
    fn $name() {
      let mut isolate = $crate::util::Isolate::default();
      let code = if $code.starts_with("{") {
        format!("({})", $code)
      } else {
        $code.to_string()
      };
      let eval_value = isolate.eval(code.as_str()).expect("eval failed");
      let eval_bytes = isolate
        .serialize_value(eval_value)
        .expect("serialize_value failed");
      println!("eval_bytes {:?}", eval_bytes);
      let de = v8_valueserializer::ValueDeserializer::default();
      let (value, heap) = de.read(&eval_bytes).expect("parse_v8 failed");
      let serialization = v8_valueserializer::display(&heap, &value, v8_valueserializer::DisplayOptions {
        format: v8_valueserializer::DisplayFormat::Repl,
      });
      insta::with_settings!({
        description => format!("=== SOURCE ===\n{}", $code),
        omit_expression => true,
      }, {
        insta::assert_snapshot!(stringify!($name), serialization);
      })
    }
  };
}