From 0aec3cf3b6bb8d83d1dba1143a436e4bbb179238 Mon Sep 17 00:00:00 2001 From: Jason Zhang Date: Sat, 8 Mar 2025 18:44:02 +1030 Subject: [PATCH] src: support (de)serialization of `DOMException` --- src/node_messaging.cc | 194 ++++++++++++++++++- test/parallel/test-structuredClone-global.js | 16 ++ 2 files changed, 207 insertions(+), 3 deletions(-) diff --git a/src/node_messaging.cc b/src/node_messaging.cc index 8fe7defa5bc02f..5725d9bf18406a 100644 --- a/src/node_messaging.cc +++ b/src/node_messaging.cc @@ -66,6 +66,10 @@ bool Message::IsCloseMessage() const { namespace { +MaybeLocal GetDOMException(Local 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 { @@ -83,11 +87,66 @@ class DeserializerDelegate : public ValueDeserializer::Delegate { wasm_modules_(wasm_modules), shared_value_conveyor_(shared_value_conveyor) {} + MaybeLocal ReadDOMException(Isolate* isolate, + Local context, + v8::ValueDeserializer* deserializer) { + Local name, message; + if (!deserializer->ReadValue(context).ToLocal(&name) || + !deserializer->ReadValue(context).ToLocal(&message)) { + return MaybeLocal(); + } + + bool has_code = false; + Local 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 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(); + } + + return placeholder; + } + MaybeLocal ReadHostObject(Isolate* isolate) override { // Identifying the index in the message's BaseObject array is sufficient. uint32_t id; if (!deserializer->ReadUint32(&id)) return MaybeLocal(); + + Local context = isolate->GetCurrentContext(); + if (id == kDOMExceptionTag) { + return ReadDOMException(isolate, context, deserializer); + } + if (id != kNormalObject) { CHECK_LT(id, host_objects_.size()); Local object = host_objects_[id]->object(isolate); @@ -98,7 +157,6 @@ class DeserializerDelegate : public ValueDeserializer::Delegate { } } EscapableHandleScope scope(isolate); - Local context = isolate->GetCurrentContext(); Local object; if (!deserializer->ReadValue(context).ToLocal(&object)) return MaybeLocal(); @@ -136,6 +194,71 @@ class DeserializerDelegate : public ValueDeserializer::Delegate { } // anonymous namespace +MaybeLocal ConvertDOMExceptionData(Local context, + Local value) { + if (!value->IsObject()) return MaybeLocal(); + + Isolate* isolate = context->GetIsolate(); + Local obj = value.As(); + + Local marker_key = + String::NewFromUtf8(isolate, "__domexception_placeholder") + .ToLocalChecked(); + Local marker_val; + if (!obj->Get(context, marker_key).ToLocal(&marker_val) || + !marker_val->IsTrue()) { + return MaybeLocal(); + } + + Local name_key = + String::NewFromUtf8(isolate, "__domexception_name").ToLocalChecked(); + Local message_key = + String::NewFromUtf8(isolate, "__domexception_message").ToLocalChecked(); + Local code_key = + String::NewFromUtf8(isolate, "__domexception_code").ToLocalChecked(); + + Local name, message, code; + if (!obj->Get(context, name_key).ToLocal(&name) || + !obj->Get(context, message_key).ToLocal(&message)) { + return MaybeLocal(); + } + bool has_code = obj->Get(context, code_key).ToLocal(&code); + + Local dom_exception_ctor; + if (!GetDOMException(context).ToLocal(&dom_exception_ctor)) { + return MaybeLocal(); + } + + // Create arguments for the constructor according to the JS implementation + // First arg: message + // Second arg: options object with name and potentially code + Local options = Object::New(isolate); + if (options + ->Set(context, + String::NewFromUtf8(isolate, "name").ToLocalChecked(), + name) + .IsNothing()) { + return MaybeLocal(); + } + + if (has_code && + options + ->Set(context, + String::NewFromUtf8(isolate, "code").ToLocalChecked(), + code) + .IsNothing()) { + return MaybeLocal(); + } + + Local argv[2] = {message, options}; + Local exception; + if (!dom_exception_ctor->NewInstance(context, 2, argv).ToLocal(&exception)) { + return MaybeLocal(); + } + + return exception.As(); +} + MaybeLocal Message::Deserialize(Environment* env, Local context, Local* port_list) { @@ -227,8 +350,14 @@ MaybeLocal Message::Deserialize(Environment* env, return {}; } - host_objects.clear(); - return handle_scope.Escape(return_value); + Local 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( @@ -294,6 +423,37 @@ void ThrowDataCloneException(Local context, Local message) { isolate->ThrowException(exception); } +Maybe IsDOMException(Isolate* isolate, + Local context, + Local obj) { + HandleScope handle_scope(isolate); + + Local per_context_bindings; + Local dom_exception_ctor_val; + + if (!GetPerContextExports(context).ToLocal(&per_context_bindings)) { + return Nothing(); + } + + if (!per_context_bindings + ->Get(context, + String::NewFromUtf8(isolate, "DOMException").ToLocalChecked()) + .ToLocal(&dom_exception_ctor_val) || + !dom_exception_ctor_val->IsFunction()) { + return Nothing(); + } + + Local dom_exception_ctor = dom_exception_ctor_val.As(); + + Maybe result = obj->InstanceOf(context, dom_exception_ctor); + + if (result.IsNothing()) { + return Nothing(); + } + + 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. @@ -313,6 +473,9 @@ class SerializerDelegate : public ValueSerializer::Delegate { return Just(true); } + Maybe 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)); } @@ -328,6 +491,11 @@ class SerializerDelegate : public ValueSerializer::Delegate { return WriteHostObject(js_transferable); } + Maybe 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() && @@ -424,6 +592,26 @@ class SerializerDelegate : public ValueSerializer::Delegate { ValueSerializer* serializer = nullptr; private: + Maybe WriteDOMException(Local context, + Local exception) { + serializer->WriteUint32(kDOMExceptionTag); + + Local 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(); + } + + if (serializer->WriteValue(context, name_val).IsNothing() || + serializer->WriteValue(context, message_val).IsNothing() || + serializer->WriteValue(context, code_val).IsNothing()) { + return Nothing(); + } + + return Just(true); + } Maybe WriteHostObject(BaseObjectPtr host_object) { BaseObject::TransferMode mode = host_object->GetTransferMode(); if (mode == TransferMode::kDisallowCloneAndTransfer) { diff --git a/test/parallel/test-structuredClone-global.js b/test/parallel/test-structuredClone-global.js index e6b63c382b39b1..102971228b9c21 100644 --- a/test/parallel/test-structuredClone-global.js +++ b/test/parallel/test-structuredClone-global.js @@ -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' });