diff options
-rw-r--r-- | doc/api/buffer.md | 14 | ||||
-rw-r--r-- | doc/api/url.md | 47 | ||||
-rw-r--r-- | lib/buffer.js | 2 | ||||
-rw-r--r-- | lib/internal/blob.js | 94 | ||||
-rw-r--r-- | lib/internal/url.js | 79 | ||||
-rw-r--r-- | src/node_binding.cc | 1 | ||||
-rw-r--r-- | src/node_blob.cc | 166 | ||||
-rw-r--r-- | src/node_blob.h | 56 | ||||
-rw-r--r-- | src/node_buffer.cc | 5 | ||||
-rw-r--r-- | src/node_external_reference.h | 1 | ||||
-rw-r--r-- | src/node_snapshotable.cc | 1 | ||||
-rw-r--r-- | src/node_snapshotable.h | 3 | ||||
-rw-r--r-- | test/parallel/test-blob-createobjecturl.js | 48 | ||||
-rw-r--r-- | test/parallel/test-bootstrap-modules.js | 1 |
14 files changed, 482 insertions, 36 deletions
diff --git a/doc/api/buffer.md b/doc/api/buffer.md index 7106d5c6b36..f419be1181a 100644 --- a/doc/api/buffer.md +++ b/doc/api/buffer.md @@ -4952,6 +4952,20 @@ added: v3.0.0 An alias for [`buffer.constants.MAX_STRING_LENGTH`][]. +### `buffer.resolveObjectURL(id)` +<!-- YAML +added: REPLACEME +--> + +> Stability: 1 - Experimental + +* `id` {string} A `'blob:nodedata:...` URL string returned by a prior call to + `URL.createObjectURL()`. +* Returns: {Blob} + +Resolves a `'blob:nodedata:...'` an associated {Blob} object registered using +a prior call to `URL.createObjectURL()`. + ### `buffer.transcode(source, fromEnc, toEnc)` <!-- YAML added: v7.1.0 diff --git a/doc/api/url.md b/doc/api/url.md index 725084523df..d4b212f515d 100644 --- a/doc/api/url.md +++ b/doc/api/url.md @@ -608,6 +608,53 @@ console.log(JSON.stringify(myURLs)); // Prints ["https://www.example.com/","https://test.example.org/"] ``` +#### `URL.createObjectURL(blob)` +<!-- YAML +added: REPLACEME +--> + +> Stability: 1 - Experimental + +* `blob` {Blob} +* Returns: {string} + +Creates a `'blob:nodedata:...'` URL string that represents the given {Blob} +object and can be used to retrieve the `Blob` later. + +```js +const { + Blob, + resolveObjectURL, +} = require('buffer'); + +const blob = new Blob(['hello']); +const id = URL.createObjectURL(blob); + +// later... + +const otherBlob = resolveObjectURL(id); +console.log(otherBlob.size); +``` + +The data stored by the registered {Blob} will be retained in memory until +`URL.revokeObjectURL()` is called to remove it. + +`Blob` objects are registered within the current thread. If using Worker +Threads, `Blob` objects registered within one Worker will not be available +to other workers or the main thread. + +#### `URL.revokeObjectURL(id)` +<!-- YAML +added: REPLACEME +--> + +> Stability: 1 - Experimental + +* `id` {string} A `'blob:nodedata:...` URL string returned by a prior call to + `URL.createObjectURL()`. + +Removes the stored {Blob} identified by the given ID. + ### Class: `URLSearchParams` <!-- YAML added: diff --git a/lib/buffer.js b/lib/buffer.js index 278a67cbbfb..1f4f0a2e89d 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -120,6 +120,7 @@ const { const { Blob, + resolveObjectURL, } = require('internal/blob'); FastBuffer.prototype.constructor = Buffer; @@ -1239,6 +1240,7 @@ function atob(input) { module.exports = { Blob, + resolveObjectURL, Buffer, SlowBuffer, transcode, diff --git a/lib/internal/blob.js b/lib/internal/blob.js index d69c9c17980..3c6e841acda 100644 --- a/lib/internal/blob.js +++ b/lib/internal/blob.js @@ -7,10 +7,11 @@ const { ObjectDefineProperty, PromiseResolve, PromiseReject, - PromisePrototypeFinally, + SafePromisePrototypeFinally, ReflectConstruct, RegExpPrototypeTest, StringPrototypeToLowerCase, + StringPrototypeSplit, Symbol, SymbolIterator, SymbolToStringTag, @@ -20,7 +21,8 @@ const { const { createBlob: _createBlob, FixedSizeBlobCopyJob, -} = internalBinding('buffer'); + getDataObject, +} = internalBinding('blob'); const { TextDecoder } = require('internal/encoding'); @@ -57,26 +59,37 @@ const { } = require('internal/validators'); const kHandle = Symbol('kHandle'); +const kState = Symbol('kState'); const kType = Symbol('kType'); const kLength = Symbol('kLength'); const kArrayBufferPromise = Symbol('kArrayBufferPromise'); +const kMaxChunkSize = 65536; + const disallowedTypeCharacters = /[^\u{0020}-\u{007E}]/u; let Buffer; let ReadableStream; +let URL; + + +// Yes, lazy loading is annoying but because of circular +// references between the url, internal/blob, and buffer +// modules, lazy loading here makes sure that things work. + +function lazyURL(id) { + URL ??= require('internal/url').URL; + return new URL(id); +} function lazyBuffer() { - if (Buffer === undefined) - Buffer = require('buffer').Buffer; + Buffer ??= require('buffer').Buffer; return Buffer; } function lazyReadableStream(options) { - if (ReadableStream === undefined) { - ReadableStream = - require('internal/webstreams/readablestream').ReadableStream; - } + ReadableStream ??= + require('internal/webstreams/readablestream').ReadableStream; return new ReadableStream(options); } @@ -232,9 +245,9 @@ class Blob { return PromiseReject(new ERR_INVALID_THIS('Blob')); // If there's already a promise in flight for the content, - // reuse it, but only once. After the cached promise resolves - // it will be cleared, allowing it to be garbage collected - // as soon as possible. + // reuse it, but only while it's in flight. After the cached + // promise resolves it will be cleared, allowing it to be + // garbage collected as soon as possible. if (this[kArrayBufferPromise]) return this[kArrayBufferPromise]; @@ -260,7 +273,7 @@ class Blob { resolve(ab); }; this[kArrayBufferPromise] = - PromisePrototypeFinally( + SafePromisePrototypeFinally( promise, () => this[kArrayBufferPromise] = undefined); @@ -268,7 +281,6 @@ class Blob { } /** - * * @returns {Promise<string>} */ async text() { @@ -288,10 +300,20 @@ class Blob { const self = this; return new lazyReadableStream({ - async start(controller) { - const ab = await self.arrayBuffer(); - controller.enqueue(new Uint8Array(ab)); - controller.close(); + async start() { + this[kState] = await self.arrayBuffer(); + }, + + pull(controller) { + if (this[kState].byteLength <= kMaxChunkSize) { + controller.enqueue(new Uint8Array(this[kState])); + controller.close(); + this[kState] = undefined; + } else { + const slice = this[kState].slice(0, kMaxChunkSize); + this[kState] = this[kState].slice(kMaxChunkSize); + controller.enqueue(new Uint8Array(slice)); + } } }); } @@ -315,9 +337,47 @@ ObjectDefineProperty(Blob.prototype, SymbolToStringTag, { value: 'Blob', }); +function resolveObjectURL(url) { + url = `${url}`; + try { + const parsed = new lazyURL(url); + + const split = StringPrototypeSplit(parsed.pathname, ':'); + + if (split.length !== 2) + return; + + const { + 0: base, + 1: id, + } = split; + + if (base !== 'nodedata') + return; + + const ret = getDataObject(id); + + if (ret === undefined) + return; + + const { + 0: handle, + 1: length, + 2: type, + } = ret; + + if (handle !== undefined) + return createBlob(handle, length, type); + } catch { + // If there's an error, it's ignored and nothing is returned + } +} + module.exports = { Blob, ClonedBlob, createBlob, isBlob, + kHandle, + resolveObjectURL, }; diff --git a/lib/internal/url.js b/lib/internal/url.js index 0749e07d6e7..043b7311a2c 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -42,17 +42,20 @@ const { const { getConstructorOf, removeColors } = require('internal/util'); const { - ERR_ARG_NOT_ITERABLE, - ERR_INVALID_ARG_TYPE, - ERR_INVALID_ARG_VALUE, - ERR_INVALID_FILE_URL_HOST, - ERR_INVALID_FILE_URL_PATH, - ERR_INVALID_THIS, - ERR_INVALID_TUPLE, - ERR_INVALID_URL, - ERR_INVALID_URL_SCHEME, - ERR_MISSING_ARGS -} = require('internal/errors').codes; + codes: { + ERR_ARG_NOT_ITERABLE, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + ERR_INVALID_FILE_URL_HOST, + ERR_INVALID_FILE_URL_PATH, + ERR_INVALID_THIS, + ERR_INVALID_TUPLE, + ERR_INVALID_URL, + ERR_INVALID_URL_SCHEME, + ERR_MISSING_ARGS, + ERR_NO_CRYPTO, + }, +} = require('internal/errors'); const { CHAR_AMPERSAND, CHAR_BACKWARD_SLASH, @@ -100,6 +103,11 @@ const { kSchemeStart } = internalBinding('url'); +const { + storeDataObject, + revokeDataObject, +} = internalBinding('blob'); + const context = Symbol('context'); const cannotBeBase = Symbol('cannot-be-base'); const cannotHaveUsernamePasswordPort = @@ -108,6 +116,24 @@ const special = Symbol('special'); const searchParams = Symbol('query'); const kFormat = Symbol('format'); +let blob; +let cryptoRandom; + +function lazyBlob() { + blob ??= require('internal/blob'); + return blob; +} + +function lazyCryptoRandom() { + try { + cryptoRandom ??= require('internal/crypto/random'); + } catch { + // If Node.js built without crypto support, we'll fall + // through here and handle it later. + } + return cryptoRandom; +} + // https://tc39.github.io/ecma262/#sec-%iteratorprototype%-object const IteratorPrototype = ObjectGetPrototypeOf( ObjectGetPrototypeOf([][SymbolIterator]()) @@ -930,6 +956,37 @@ class URL { toJSON() { return this[kFormat]({}); } + + static createObjectURL(obj) { + const cryptoRandom = lazyCryptoRandom(); + if (cryptoRandom === undefined) + throw new ERR_NO_CRYPTO(); + + // Yes, lazy loading is annoying but because of circular + // references between the url, internal/blob, and buffer + // modules, lazy loading here makes sure that things work. + const blob = lazyBlob(); + if (!blob.isBlob(obj)) + throw new ERR_INVALID_ARG_TYPE('obj', 'Blob', obj); + + const id = cryptoRandom.randomUUID(); + + storeDataObject(id, obj[blob.kHandle], obj.size, obj.type); + + return `blob:nodedata:${id}`; + } + + static revokeObjectURL(url) { + url = `${url}`; + try { + const parsed = new URL(url); + const split = StringPrototypeSplit(parsed.pathname, ':'); + if (split.length === 2) + revokeDataObject(split[1]); + } catch { + // If there's an error, it's ignored. + } + } } ObjectDefineProperties(URL.prototype, { diff --git a/src/node_binding.cc b/src/node_binding.cc index b5e42af7951..3c9776e655d 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -40,6 +40,7 @@ // __attribute__((constructor)) like mechanism in GCC. #define NODE_BUILTIN_STANDARD_MODULES(V) \ V(async_wrap) \ + V(blob) \ V(block_list) \ V(buffer) \ V(cares_wrap) \ diff --git a/src/node_blob.cc b/src/node_blob.cc index 0b18bbce138..c583d5e0a93 100644 --- a/src/node_blob.cc +++ b/src/node_blob.cc @@ -26,12 +26,26 @@ using v8::Local; using v8::MaybeLocal; using v8::Number; using v8::Object; +using v8::String; using v8::Uint32; using v8::Undefined; using v8::Value; -void Blob::Initialize(Environment* env, v8::Local<v8::Object> target) { +void Blob::Initialize( + Local<Object> target, + Local<Value> unused, + Local<Context> context, + void* priv) { + Environment* env = Environment::GetCurrent(context); + + BlobBindingData* const binding_data = + env->AddBindingData<BlobBindingData>(context, target); + if (binding_data == nullptr) return; + env->SetMethod(target, "createBlob", New); + env->SetMethod(target, "storeDataObject", StoreDataObject); + env->SetMethod(target, "getDataObject", GetDataObject); + env->SetMethod(target, "revokeDataObject", RevokeDataObject); FixedSizeBlobCopyJob::Initialize(env, target); } @@ -220,6 +234,78 @@ std::unique_ptr<worker::TransferData> Blob::CloneForMessaging() const { return std::make_unique<BlobTransferData>(store_, length_); } +void Blob::StoreDataObject(const v8::FunctionCallbackInfo<v8::Value>& args) { + Environment* env = Environment::GetCurrent(args); + BlobBindingData* binding_data = + Environment::GetBindingData<BlobBindingData>(args); + + CHECK(args[0]->IsString()); // ID key + CHECK(Blob::HasInstance(env, args[1])); // Blob + CHECK(args[2]->IsUint32()); // Length + CHECK(args[3]->IsString()); // Type + + Utf8Value key(env->isolate(), args[0]); + Blob* blob; + ASSIGN_OR_RETURN_UNWRAP(&blob, args[1]); + + size_t length = args[2].As<Uint32>()->Value(); + Utf8Value type(env->isolate(), args[3]); + + binding_data->store_data_object( + std::string(*key, key.length()), + BlobBindingData::StoredDataObject( + BaseObjectPtr<Blob>(blob), + length, + std::string(*type, type.length()))); +} + +void Blob::RevokeDataObject(const v8::FunctionCallbackInfo<v8::Value>& args) { + BlobBindingData* binding_data = + Environment::GetBindingData<BlobBindingData>(args); + + Environment* env = Environment::GetCurrent(args); + CHECK(args[0]->IsString()); // ID key + + Utf8Value key(env->isolate(), args[0]); + + binding_data->revoke_data_object(std::string(*key, key.length())); +} + +void Blob::GetDataObject(const v8::FunctionCallbackInfo<v8::Value>& args) { + BlobBindingData* binding_data = + Environment::GetBindingData<BlobBindingData>(args); + + Environment* env = Environment::GetCurrent(args); + CHECK(args[0]->IsString()); + + Utf8Value key(env->isolate(), args[0]); + + BlobBindingData::StoredDataObject stored = + binding_data->get_data_object(std::string(*key, key.length())); + if (stored.blob) { + Local<Value> type; + if (!String::NewFromUtf8( + env->isolate(), + stored.type.c_str(), + v8::NewStringType::kNormal, + static_cast<int>(stored.type.length())).ToLocal(&type)) { + return; + } + + Local<Value> values[] = { + stored.blob->object(), + Uint32::NewFromUnsigned(env->isolate(), stored.length), + type + }; + + args.GetReturnValue().Set( + Array::New( + env->isolate(), + values, + arraysize(values))); + } +} + FixedSizeBlobCopyJob::FixedSizeBlobCopyJob( Environment* env, Local<Object> object, @@ -328,10 +414,88 @@ void FixedSizeBlobCopyJob::RegisterExternalReferences( registry->Register(Run); } +void BlobBindingData::StoredDataObject::MemoryInfo( + MemoryTracker* tracker) const { + tracker->TrackField("blob", blob); + tracker->TrackFieldWithSize("type", type.length()); +} + +BlobBindingData::StoredDataObject::StoredDataObject( + const BaseObjectPtr<Blob>& blob_, + size_t length_, + const std::string& type_) + : blob(blob_), + length(length_), + type(type_) {} + +BlobBindingData::BlobBindingData(Environment* env, Local<Object> wrap) + : SnapshotableObject(env, wrap, type_int) { + MakeWeak(); +} + +void BlobBindingData::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("data_objects", data_objects_); +} + +void BlobBindingData::store_data_object( + const std::string& uuid, + const BlobBindingData::StoredDataObject& object) { + data_objects_[uuid] = object; +} + +void BlobBindingData::revoke_data_object(const std::string& uuid) { + CHECK_NE(data_objects_.find(uuid), data_objects_.end()); + data_objects_.erase(uuid); + CHECK_EQ(data_objects_.find(uuid), data_objects_.end()); +} + +BlobBindingData::StoredDataObject BlobBindingData::get_data_object( + const std::string& uuid) { + auto entry = data_objects_.find(uuid); + if (entry == data_objects_.end()) + return BlobBindingData::StoredDataObject {}; + return entry->second; +} + +void BlobBindingData::Deserialize( + Local<Context> context, + Local<Object> holder, + int index, + InternalFieldInfo* info) { + DCHECK_EQ(index, BaseObject::kSlot); + HandleScope scope(context->GetIsolate()); + Environment* env = Environment::GetCurrent(context); + BlobBindingData* binding = + env->AddBindingData<BlobBindingData>(context, holder); + CHECK_NOT_NULL(binding); +} + +void BlobBindingData::PrepareForSerialization( + Local<Context> context, + v8::SnapshotCreator* creator) { + // Stored blob objects are not actually persisted. +} + +InternalFieldInfo* BlobBindingData::Serialize(int index) { + DCHECK_EQ(index, BaseObject::kSlot); + InternalFieldInfo* info = InternalFieldInfo::New(type()); + return info; +} + +constexpr FastStringKey BlobBindingData::type_name; + void Blob::RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(Blob::New); registry->Register(Blob::ToArrayBuffer); registry->Register(Blob::ToSlice); + registry->Register(Blob::StoreDataObject); + registry->Register(Blob::GetDataObject); + registry->Register(Blob::RevokeDataObject); + + FixedSizeBlobCopyJob::RegisterExternalReferences(registry); } } // namespace node + +NODE_MODULE_CONTEXT_AWARE_INTERNAL(blob, node::Blob::Initialize); +NODE_MODULE_EXTERNAL_REFERENCE(blob, node::Blob::RegisterExternalReferences); diff --git a/src/node_blob.h b/src/node_blob.h index 9d6178996c8..61f4da655c9 100644 --- a/src/node_blob.h +++ b/src/node_blob.h @@ -8,9 +8,12 @@ #include "env.h" #include "memory_tracker.h" #include "node_internals.h" +#include "node_snapshotable.h" #include "node_worker.h" #include "v8.h" +#include <string> +#include <unordered_map> #include <vector> namespace node { @@ -25,11 +28,19 @@ class Blob : public BaseObject { public: static void RegisterExternalReferences( ExternalReferenceRegistry* registry); - static void Initialize(Environment* env, v8::Local<v8::Object> target); + + static void Initialize( + v8::Local<v8::Object> target, + v8::Local<v8::Value> unused, + v8::Local<v8::Context> context, + void* priv); static void New(const v8::FunctionCallbackInfo<v8::Value>& args); static void ToArrayBuffer(const v8::FunctionCallbackInfo<v8::Value>& args); static void ToSlice(const v8::FunctionCallbackInfo<v8::Value>& args); + static void StoreDataObject(const v8::FunctionCallbackInfo<v8::Value>& args); + static void GetDataObject(const v8::FunctionCallbackInfo<v8::Value>& args); + static void RevokeDataObject(const v8::FunctionCallbackInfo<v8::Value>& args); static v8::Local<v8::FunctionTemplate> GetConstructorTemplate( Environment* env); @@ -131,6 +142,49 @@ class FixedSizeBlobCopyJob : public AsyncWrap, public ThreadPoolWork { size_t length_ = 0; }; +class BlobBindingData : public SnapshotableObject { + public: + explicit BlobBindingData(Environment* env, v8::Local<v8::Object> wrap); + + SERIALIZABLE_OBJECT_METHODS() + + static constexpr FastStringKey type_name{"node::BlobBindingData"}; + static constexpr EmbedderObjectType type_int = + EmbedderObjectType::k_blob_binding_data; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_SELF_SIZE(BlobBindingData) + SET_MEMORY_INFO_NAME(BlobBindingData) + + struct StoredDataObject : public MemoryRetainer { + BaseObjectPtr<Blob> blob; + size_t length; + std::string type; + + StoredDataObject() = default; + + StoredDataObject( + const BaseObjectPtr<Blob>& blob_, + size_t length_, + const std::string& type_); + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_SELF_SIZE(StoredDataObject) + SET_MEMORY_INFO_NAME(StoredDataObject) + }; + + void store_data_object( + const std::string& uuid, + const StoredDataObject& object); + + void revoke_data_object(const std::string& uuid); + + StoredDataObject get_data_object(const std::string& uuid); + + private: + std::unordered_map<std::string, StoredDataObject> data_objects_; +}; + } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/node_buffer.cc b/src/node_buffer.cc index b5651b5e325..962a4a11aab 100644 --- a/src/node_buffer.cc +++ b/src/node_buffer.cc @@ -1266,8 +1266,6 @@ void Initialize(Local<Object> target, env->SetMethod(target, "utf8Write", StringWrite<UTF8>); env->SetMethod(target, "getZeroFillToggle", GetZeroFillToggle); - - Blob::Initialize(env, target); } } // anonymous namespace @@ -1311,9 +1309,6 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(DetachArrayBuffer); registry->Register(CopyArrayBuffer); - - Blob::RegisterExternalReferences(registry); - FixedSizeBlobCopyJob::RegisterExternalReferences(registry); } } // namespace Buffer diff --git a/src/node_external_reference.h b/src/node_external_reference.h index 094558783f7..8d2de64ad43 100644 --- a/src/node_external_reference.h +++ b/src/node_external_reference.h @@ -49,6 +49,7 @@ class ExternalReferenceRegistry { #define EXTERNAL_REFERENCE_BINDING_LIST_BASE(V) \ V(async_wrap) \ V(binding) \ + V(blob) \ V(buffer) \ V(contextify) \ V(credentials) \ diff --git a/src/node_snapshotable.cc b/src/node_snapshotable.cc index 35e0ed3f6df..6e284bb66a0 100644 --- a/src/node_snapshotable.cc +++ b/src/node_snapshotable.cc @@ -5,6 +5,7 @@ #include "base_object-inl.h" #include "debug_utils-inl.h" #include "env-inl.h" +#include "node_blob.h" #include "node_errors.h" #include "node_external_reference.h" #include "node_file.h" diff --git a/src/node_snapshotable.h b/src/node_snapshotable.h index 38da68f6d28..ceb84fc9bf0 100644 --- a/src/node_snapshotable.h +++ b/src/node_snapshotable.h @@ -15,7 +15,8 @@ struct SnapshotData; #define SERIALIZABLE_OBJECT_TYPES(V) \ V(fs_binding_data, fs::BindingData) \ - V(v8_binding_data, v8_utils::BindingData) + V(v8_binding_data, v8_utils::BindingData) \ + V(blob_binding_data, BlobBindingData) enum class EmbedderObjectType : uint8_t { k_default = 0, diff --git a/test/parallel/test-blob-createobjecturl.js b/test/parallel/test-blob-createobjecturl.js new file mode 100644 index 00000000000..a8fd377dd3e --- /dev/null +++ b/test/parallel/test-blob-createobjecturl.js @@ -0,0 +1,48 @@ +// Flags: --no-warnings +'use strict'; + +const common = require('../common'); + +// Because registering a Blob URL requires generating a random +// UUID, it can only be done if crypto support is enabled. +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { + URL, +} = require('url'); + +const { + Blob, + resolveObjectURL, +} = require('buffer'); + +const assert = require('assert'); + +(async () => { + const blob = new Blob(['hello']); + const id = URL.createObjectURL(blob); + assert.strictEqual(typeof id, 'string'); + const otherBlob = resolveObjectURL(id); + assert.strictEqual(otherBlob.size, 5); + assert.strictEqual( + Buffer.from(await otherBlob.arrayBuffer()).toString(), + 'hello'); + URL.revokeObjectURL(id); + assert.strictEqual(resolveObjectURL(id), undefined); + + // Leaving a Blob registered should not cause an assert + // when Node.js exists + URL.createObjectURL(new Blob()); + +})().then(common.mustCall()); + +['not a url', undefined, 1, 'blob:nodedata:1:wrong', {}].forEach((i) => { + assert.strictEqual(resolveObjectURL(i), undefined); +}); + +[undefined, 1, '', false, {}].forEach((i) => { + assert.throws(() => URL.createObjectURL(i), { + code: 'ERR_INVALID_ARG_TYPE', + }); +}); diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index d02f0c71860..37cdbca3300 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -130,6 +130,7 @@ const expectedModules = new Set([ 'NativeModule internal/vm/module', 'NativeModule internal/worker/io', 'NativeModule internal/worker/js_transferable', + 'Internal Binding blob', 'NativeModule internal/blob', 'NativeModule async_hooks', 'NativeModule path', |