Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

src: support (de)serialization of DOMException #57372

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 193 additions & 3 deletions src/node_messaging.cc
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ bool Message::IsCloseMessage() const {

namespace {

MaybeLocal<Function> GetDOMException(Local<Context> context);

static const uint32_t kDOMExceptionTag = 0xD011;

// This is used to tell V8 how to read transferred host objects, like other
// `MessagePort`s and `SharedArrayBuffer`s, and make new JS objects out of them.
class DeserializerDelegate : public ValueDeserializer::Delegate {
Expand All @@ -83,11 +87,66 @@ class DeserializerDelegate : public ValueDeserializer::Delegate {
wasm_modules_(wasm_modules),
shared_value_conveyor_(shared_value_conveyor) {}

MaybeLocal<Object> ReadDOMException(Isolate* isolate,
Local<Context> context,
v8::ValueDeserializer* deserializer) {
Local<Value> name, message;
if (!deserializer->ReadValue(context).ToLocal(&name) ||
!deserializer->ReadValue(context).ToLocal(&message)) {
return MaybeLocal<Object>();
}

bool has_code = false;
Local<Value> code;
has_code = deserializer->ReadValue(context).ToLocal(&code);

// V8 disallows executing JS code in the deserialization process, so we
// cannot create a DOMException object directly. Instead, we create a
// placeholder object that will be converted to a DOMException object
// later on.
Local<Object> placeholder = Object::New(isolate);
if (placeholder
->Set(context,
String::NewFromUtf8(isolate, "__domexception_name")
.ToLocalChecked(),
name)
.IsNothing() ||
placeholder
->Set(context,
String::NewFromUtf8(isolate, "__domexception_message")
.ToLocalChecked(),
message)
.IsNothing() ||
(has_code &&
placeholder
->Set(context,
String::NewFromUtf8(isolate, "__domexception_code")
.ToLocalChecked(),
code)
.IsNothing()) ||
placeholder
->Set(context,
String::NewFromUtf8(isolate, "__domexception_placeholder")
.ToLocalChecked(),
v8::True(isolate))
.IsNothing()) {
return MaybeLocal<Object>();
}

return placeholder;
}

MaybeLocal<Object> ReadHostObject(Isolate* isolate) override {
// Identifying the index in the message's BaseObject array is sufficient.
uint32_t id;
if (!deserializer->ReadUint32(&id))
return MaybeLocal<Object>();

Local<Context> context = isolate->GetCurrentContext();
if (id == kDOMExceptionTag) {
return ReadDOMException(isolate, context, deserializer);
}

if (id != kNormalObject) {
CHECK_LT(id, host_objects_.size());
Local<Object> object = host_objects_[id]->object(isolate);
Expand All @@ -98,7 +157,6 @@ class DeserializerDelegate : public ValueDeserializer::Delegate {
}
}
EscapableHandleScope scope(isolate);
Local<Context> context = isolate->GetCurrentContext();
Local<Value> object;
if (!deserializer->ReadValue(context).ToLocal(&object))
return MaybeLocal<Object>();
Expand Down Expand Up @@ -136,6 +194,71 @@ class DeserializerDelegate : public ValueDeserializer::Delegate {

} // anonymous namespace

MaybeLocal<Object> ConvertDOMExceptionData(Local<Context> context,
Local<Value> value) {
if (!value->IsObject()) return MaybeLocal<Object>();

Isolate* isolate = context->GetIsolate();
Local<Object> obj = value.As<Object>();

Local<String> marker_key =
String::NewFromUtf8(isolate, "__domexception_placeholder")
.ToLocalChecked();
Local<Value> marker_val;
if (!obj->Get(context, marker_key).ToLocal(&marker_val) ||
!marker_val->IsTrue()) {
return MaybeLocal<Object>();
}

Local<String> name_key =
String::NewFromUtf8(isolate, "__domexception_name").ToLocalChecked();
Local<String> message_key =
String::NewFromUtf8(isolate, "__domexception_message").ToLocalChecked();
Local<String> code_key =
String::NewFromUtf8(isolate, "__domexception_code").ToLocalChecked();

Local<Value> name, message, code;
if (!obj->Get(context, name_key).ToLocal(&name) ||
!obj->Get(context, message_key).ToLocal(&message)) {
return MaybeLocal<Object>();
}
bool has_code = obj->Get(context, code_key).ToLocal(&code);

Local<Function> dom_exception_ctor;
if (!GetDOMException(context).ToLocal(&dom_exception_ctor)) {
return MaybeLocal<Object>();
}

// Create arguments for the constructor according to the JS implementation
// First arg: message
// Second arg: options object with name and potentially code
Local<Object> options = Object::New(isolate);
if (options
->Set(context,
String::NewFromUtf8(isolate, "name").ToLocalChecked(),
name)
.IsNothing()) {
return MaybeLocal<Object>();
}

if (has_code &&
options
->Set(context,
String::NewFromUtf8(isolate, "code").ToLocalChecked(),
code)
.IsNothing()) {
return MaybeLocal<Object>();
}

Local<Value> argv[2] = {message, options};
Local<Value> exception;
if (!dom_exception_ctor->NewInstance(context, 2, argv).ToLocal(&exception)) {
return MaybeLocal<Object>();
}

return exception.As<Object>();
}

MaybeLocal<Value> Message::Deserialize(Environment* env,
Local<Context> context,
Local<Value>* port_list) {
Expand Down Expand Up @@ -227,8 +350,14 @@ MaybeLocal<Value> Message::Deserialize(Environment* env,
return {};
}

host_objects.clear();
return handle_scope.Escape(return_value);
Local<Object> converted_dom_exception;
if (!ConvertDOMExceptionData(context, return_value)
.ToLocal(&converted_dom_exception)) {
host_objects.clear();
return handle_scope.Escape(return_value);
}

return handle_scope.Escape(converted_dom_exception);
}

void Message::AddSharedArrayBuffer(
Expand Down Expand Up @@ -294,6 +423,37 @@ void ThrowDataCloneException(Local<Context> context, Local<String> message) {
isolate->ThrowException(exception);
}

Maybe<bool> IsDOMException(Isolate* isolate,
Local<Context> context,
Local<Object> obj) {
HandleScope handle_scope(isolate);

Local<Object> per_context_bindings;
Local<Value> dom_exception_ctor_val;

if (!GetPerContextExports(context).ToLocal(&per_context_bindings)) {
return Nothing<bool>();
}

if (!per_context_bindings
->Get(context,
String::NewFromUtf8(isolate, "DOMException").ToLocalChecked())
.ToLocal(&dom_exception_ctor_val) ||
!dom_exception_ctor_val->IsFunction()) {
return Nothing<bool>();
}

Local<Function> dom_exception_ctor = dom_exception_ctor_val.As<Function>();

Maybe<bool> result = obj->InstanceOf(context, dom_exception_ctor);

if (result.IsNothing()) {
return Nothing<bool>();
}

return Just(result.FromJust());
}

// This tells V8 how to serialize objects that it does not understand
// (e.g. C++ objects) into the output buffer, in a way that our own
// DeserializerDelegate understands how to unpack.
Expand All @@ -313,6 +473,11 @@ class SerializerDelegate : public ValueSerializer::Delegate {
return Just(true);
}

Maybe<bool> is_dom_exception = IsDOMException(isolate, context_, object);
if (!is_dom_exception.IsNothing() && is_dom_exception.FromJust()) {
return Just(true);
}

return Just(JSTransferable::IsJSTransferable(env_, context_, object));
}

Expand All @@ -328,6 +493,11 @@ class SerializerDelegate : public ValueSerializer::Delegate {
return WriteHostObject(js_transferable);
}

Maybe<bool> is_dom_exception = IsDOMException(isolate, context_, object);
if (!is_dom_exception.IsNothing() && is_dom_exception.FromJust()) {
return WriteDOMException(context_, object);
}

// Convert process.env to a regular object.
auto env_proxy_ctor_template = env_->env_proxy_ctor_template();
if (!env_proxy_ctor_template.IsEmpty() &&
Expand Down Expand Up @@ -424,6 +594,26 @@ class SerializerDelegate : public ValueSerializer::Delegate {
ValueSerializer* serializer = nullptr;

private:
Maybe<bool> WriteDOMException(Local<Context> context,
Local<Object> exception) {
serializer->WriteUint32(kDOMExceptionTag);

Local<Value> name_val, message_val, code_val;
if (!exception->Get(context, env_->name_string()).ToLocal(&name_val) ||
!exception->Get(context, env_->message_string())
.ToLocal(&message_val) ||
!exception->Get(context, env_->code_string()).ToLocal(&code_val)) {
return Nothing<bool>();
}

if (serializer->WriteValue(context, name_val).IsNothing() ||
serializer->WriteValue(context, message_val).IsNothing() ||
serializer->WriteValue(context, code_val).IsNothing()) {
return Nothing<bool>();
}

return Just(true);
}
Maybe<bool> WriteHostObject(BaseObjectPtr<BaseObject> host_object) {
BaseObject::TransferMode mode = host_object->GetTransferMode();
if (mode == TransferMode::kDisallowCloneAndTransfer) {
Expand Down
16 changes: 16 additions & 0 deletions test/parallel/test-structuredClone-global.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,21 @@ for (const Transferrable of [File, Blob]) {
assert.deepStrictEqual(cloned, {});
}

{
// https://github.com/nodejs/node/issues/49181
const [e, c] = (() => {
try {
structuredClone(() => {});
} catch (e) {
return [e, structuredClone(e)];
}
})();

assert.strictEqual(e instanceof Error, c instanceof Error);
assert.strictEqual(e.name, c.name);
assert.strictEqual(e.message, c.message);
assert.strictEqual(e.code, c.code);
}

const blob = new Blob();
assert.throws(() => structuredClone(blob, { transfer: [blob] }), { name: 'DataCloneError' });
Loading