From 8e2de7125b3dc855d8bb50edcc91549656ec51c7 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sun, 8 Aug 2021 14:19:44 +0200 Subject: [PATCH] buffer: introduce Blob The `Blob` object is an immutable data buffer. This is a first step towards alignment with the `Blob` Web API. Signed-off-by: James M Snell PR-URL: https://github.com/nodejs/node/pull/36811 Backport-PR-URL: https://github.com/nodejs/node/pull/39704 Reviewed-By: Antoine du Hamel Reviewed-By: Matteo Collina Reviewed-By: Benjamin Gruenbaum --- doc/api/buffer.md | 114 +++++++ lib/buffer.js | 5 + lib/internal/blob.js | 240 +++++++++++++ node.gyp | 3 + src/async_wrap.h | 1 + src/env.h | 1 + src/node_blob.cc | 323 ++++++++++++++++++ src/node_blob.h | 133 ++++++++ src/node_buffer.cc | 3 + test/parallel/test-blob.js | 187 ++++++++++ test/parallel/test-bootstrap-modules.js | 1 + test/sequential/test-async-wrap-getasyncid.js | 1 + tools/doc/type-parser.mjs | 2 + 13 files changed, 1014 insertions(+) create mode 100644 lib/internal/blob.js create mode 100644 src/node_blob.cc create mode 100644 src/node_blob.h create mode 100644 test/parallel/test-blob.js diff --git a/doc/api/buffer.md b/doc/api/buffer.md index 76c920f0b661d4..9ada48fb4f7a40 100644 --- a/doc/api/buffer.md +++ b/doc/api/buffer.md @@ -287,6 +287,119 @@ for (const b of buf) { Additionally, the [`buf.values()`][], [`buf.keys()`][], and [`buf.entries()`][] methods can be used to create iterators. +## Class: `Blob` + + +> Stability: 1 - Experimental + +A [`Blob`][] encapsulates immutable, raw data that can be safely shared across +multiple worker threads. + +### `new buffer.Blob([sources[, options]])` + + +* `sources` {string[]|ArrayBuffer[]|TypedArray[]|DataView[]|Blob[]} An array + of string, {ArrayBuffer}, {TypedArray}, {DataView}, or {Blob} objects, or + any mix of such objects, that will be stored within the `Blob`. +* `options` {Object} + * `encoding` {string} The character encoding to use for string sources. + **Default**: `'utf8'`. + * `type` {string} The Blob content-type. The intent is for `type` to convey + the MIME media type of the data, however no validation of the type format + is performed. + +Creates a new `Blob` object containing a concatenation of the given sources. + +{ArrayBuffer}, {TypedArray}, {DataView}, and {Buffer} sources are copied into +the 'Blob' and can therefore be safely modified after the 'Blob' is created. + +String sources are also copied into the `Blob`. + +### `blob.arrayBuffer()` + + +* Returns: {Promise} + +Returns a promise that fulfills with an {ArrayBuffer} containing a copy of +the `Blob` data. + +### `blob.size` + + +The total size of the `Blob` in bytes. + +### `blob.slice([start, [end, [type]]])` + + +* `start` {number} The starting index. +* `end` {number} The ending index. +* `type` {string} The content-type for the new `Blob` + +Creates and returns a new `Blob` containing a subset of this `Blob` objects +data. The original `Blob` is not alterered. + +### `blob.text()` + + +* Returns: {Promise} + +Returns a promise that resolves the contents of the `Blob` decoded as a UTF-8 +string. + +### `blob.type` + + +* Type: {string} + +The content-type of the `Blob`. + +### `Blob` objects and `MessageChannel` + +Once a {Blob} object is created, it can be sent via `MessagePort` to multiple +destinations without transfering or immediately copying the data. The data +contained by the `Blob` is copied only when the `arrayBuffer()` or `text()` +methods are called. + +```js +const { Blob } = require('buffer'); +const blob = new Blob(['hello there']); +const { setTimeout: delay } = require('timers/promises'); + +const mc1 = new MessageChannel(); +const mc2 = new MessageChannel(); + +mc1.port1.onmessage = async ({ data }) => { + console.log(await data.arrayBuffer()); + mc1.port1.close(); +}; + +mc2.port1.onmessage = async ({ data }) => { + await delay(1000); + console.log(await data.arrayBuffer()); + mc2.port1.close(); +}; + +mc1.port2.postMessage(blob); +mc2.port2.postMessage(blob); + +// The Blob is still usable after posting. +data.text().then(console.log); +``` + ## Class: `Buffer` The `Buffer` class is a global type for dealing with binary data directly. @@ -3397,6 +3510,7 @@ introducing security vulnerabilities into an application. [UTF-8]: https://en.wikipedia.org/wiki/UTF-8 [WHATWG Encoding Standard]: https://encoding.spec.whatwg.org/ [`ArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer +[`Blob`]: https://developer.mozilla.org/en-US/docs/Web/API/Blob [`Buffer.alloc()`]: #buffer_static_method_buffer_alloc_size_fill_encoding [`Buffer.allocUnsafe()`]: #buffer_static_method_buffer_allocunsafe_size [`Buffer.allocUnsafeSlow()`]: #buffer_static_method_buffer_allocunsafeslow_size diff --git a/lib/buffer.js b/lib/buffer.js index 15f6f18f49c661..757399e5a6465c 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -116,6 +116,10 @@ const { addBufferPrototypeMethods } = require('internal/buffer'); +const { + Blob, +} = require('internal/blob'); + FastBuffer.prototype.constructor = Buffer; Buffer.prototype = FastBuffer.prototype; addBufferPrototypeMethods(Buffer.prototype); @@ -1259,6 +1263,7 @@ function atob(input) { } module.exports = { + Blob, Buffer, SlowBuffer, transcode, diff --git a/lib/internal/blob.js b/lib/internal/blob.js new file mode 100644 index 00000000000000..f0220552256737 --- /dev/null +++ b/lib/internal/blob.js @@ -0,0 +1,240 @@ +'use strict'; + +const { + ArrayFrom, + ObjectSetPrototypeOf, + Promise, + PromiseResolve, + RegExpPrototypeTest, + StringPrototypeToLowerCase, + Symbol, + SymbolIterator, + Uint8Array, +} = primordials; + +const { + createBlob, + FixedSizeBlobCopyJob, +} = internalBinding('buffer'); + +const { TextDecoder } = require('internal/encoding'); + +const { + JSTransferable, + kClone, + kDeserialize, +} = require('internal/worker/js_transferable'); + +const { + isAnyArrayBuffer, + isArrayBufferView, +} = require('internal/util/types'); + +const { + customInspectSymbol: kInspect, + emitExperimentalWarning, +} = require('internal/util'); +const { inspect } = require('internal/util/inspect'); + +const { + AbortError, + codes: { + ERR_INVALID_ARG_TYPE, + ERR_BUFFER_TOO_LARGE, + ERR_OUT_OF_RANGE, + } +} = require('internal/errors'); + +const { + validateObject, + validateString, + validateUint32, + isUint32, +} = require('internal/validators'); + +const kHandle = Symbol('kHandle'); +const kType = Symbol('kType'); +const kLength = Symbol('kLength'); + +let Buffer; + +function deferred() { + let res, rej; + const promise = new Promise((resolve, reject) => { + res = resolve; + rej = reject; + }); + return { promise, resolve: res, reject: rej }; +} + +function lazyBuffer() { + if (Buffer === undefined) + Buffer = require('buffer').Buffer; + return Buffer; +} + +function isBlob(object) { + return object?.[kHandle] !== undefined; +} + +function getSource(source, encoding) { + if (isBlob(source)) + return [source.size, source[kHandle]]; + + if (typeof source === 'string') { + source = lazyBuffer().from(source, encoding); + } else if (isAnyArrayBuffer(source)) { + source = new Uint8Array(source); + } else if (!isArrayBufferView(source)) { + throw new ERR_INVALID_ARG_TYPE( + 'source', + [ + 'string', + 'ArrayBuffer', + 'SharedArrayBuffer', + 'Buffer', + 'TypedArray', + 'DataView' + ], + source); + } + + // We copy into a new Uint8Array because the underlying + // BackingStores are going to be detached and owned by + // the Blob. We also don't want to have to worry about + // byte offsets. + source = new Uint8Array(source); + return [source.byteLength, source]; +} + +class InternalBlob extends JSTransferable { + constructor(handle, length, type = '') { + super(); + this[kHandle] = handle; + this[kType] = type; + this[kLength] = length; + } +} + +class Blob extends JSTransferable { + constructor(sources = [], options) { + emitExperimentalWarning('buffer.Blob'); + if (sources === null || + typeof sources[SymbolIterator] !== 'function' || + typeof sources === 'string') { + throw new ERR_INVALID_ARG_TYPE('sources', 'Iterable', sources); + } + if (options !== undefined) + validateObject(options, 'options'); + const { + encoding = 'utf8', + type = '', + } = { ...options }; + + let length = 0; + const sources_ = ArrayFrom(sources, (source) => { + const { 0: len, 1: src } = getSource(source, encoding); + length += len; + return src; + }); + + // This is a MIME media type but we're not actively checking the syntax. + // But, to be fair, neither does Chrome. + validateString(type, 'options.type'); + + if (!isUint32(length)) + throw new ERR_BUFFER_TOO_LARGE(0xFFFFFFFF); + + super(); + this[kHandle] = createBlob(sources_, length); + this[kLength] = length; + this[kType] = RegExpPrototypeTest(/[^\u{0020}-\u{007E}]/u, type) ? + '' : StringPrototypeToLowerCase(type); + } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1 + }; + + return `Blob ${inspect({ + size: this.size, + type: this.type, + }, opts)}`; + } + + [kClone]() { + const handle = this[kHandle]; + const type = this[kType]; + const length = this[kLength]; + return { + data: { handle, type, length }, + deserializeInfo: 'internal/blob:InternalBlob' + }; + } + + [kDeserialize]({ handle, type, length }) { + this[kHandle] = handle; + this[kType] = type; + this[kLength] = length; + } + + get type() { return this[kType]; } + + get size() { return this[kLength]; } + + slice(start = 0, end = (this[kLength]), type = this[kType]) { + validateUint32(start, 'start'); + if (end < 0) end = this[kLength] + end; + validateUint32(end, 'end'); + validateString(type, 'type'); + if (end < start) + throw new ERR_OUT_OF_RANGE('end', 'greater than start', end); + if (end > this[kLength]) + throw new ERR_OUT_OF_RANGE('end', 'less than or equal to length', end); + return new InternalBlob( + this[kHandle].slice(start, end), + end - start, type); + } + + async arrayBuffer() { + const job = new FixedSizeBlobCopyJob(this[kHandle]); + + const ret = job.run(); + if (ret !== undefined) + return PromiseResolve(ret); + + const { + promise, + resolve, + reject + } = deferred(); + job.ondone = (err, ab) => { + if (err !== undefined) + return reject(new AbortError()); + resolve(ab); + }; + + return promise; + } + + async text() { + const dec = new TextDecoder(); + return dec.decode(await this.arrayBuffer()); + } +} + +InternalBlob.prototype.constructor = Blob; +ObjectSetPrototypeOf( + InternalBlob.prototype, + Blob.prototype); + +module.exports = { + Blob, + InternalBlob, + isBlob, +}; diff --git a/node.gyp b/node.gyp index 5164a4915ba992..2197f0fd755a15 100644 --- a/node.gyp +++ b/node.gyp @@ -101,6 +101,7 @@ 'lib/internal/assert/assertion_error.js', 'lib/internal/assert/calltracker.js', 'lib/internal/async_hooks.js', + 'lib/internal/blob.js', 'lib/internal/blocklist.js', 'lib/internal/buffer.js', 'lib/internal/cli_table.js', @@ -589,6 +590,7 @@ 'src/node.cc', 'src/node_api.cc', 'src/node_binding.cc', + 'src/node_blob.cc', 'src/node_buffer.cc', 'src/node_config.cc', 'src/node_constants.cc', @@ -687,6 +689,7 @@ 'src/node_api.h', 'src/node_api_types.h', 'src/node_binding.h', + 'src/node_blob.h', 'src/node_buffer.h', 'src/node_constants.h', 'src/node_context_data.h', diff --git a/src/async_wrap.h b/src/async_wrap.h index 81d0db01c950ec..374228c3fe8866 100644 --- a/src/async_wrap.h +++ b/src/async_wrap.h @@ -38,6 +38,7 @@ namespace node { V(ELDHISTOGRAM) \ V(FILEHANDLE) \ V(FILEHANDLECLOSEREQ) \ + V(FIXEDSIZEBLOBCOPY) \ V(FSEVENTWRAP) \ V(FSREQCALLBACK) \ V(FSREQPROMISE) \ diff --git a/src/env.h b/src/env.h index 10c4e0fbf72958..a6fbddefd2dbe4 100644 --- a/src/env.h +++ b/src/env.h @@ -413,6 +413,7 @@ constexpr size_t kFsStatsBufferLength = V(async_wrap_object_ctor_template, v8::FunctionTemplate) \ V(base_object_ctor_template, v8::FunctionTemplate) \ V(binding_data_ctor_template, v8::FunctionTemplate) \ + V(blob_constructor_template, v8::FunctionTemplate) \ V(blocklist_constructor_template, v8::FunctionTemplate) \ V(compiled_fn_entry_template, v8::ObjectTemplate) \ V(dir_instance_template, v8::ObjectTemplate) \ diff --git a/src/node_blob.cc b/src/node_blob.cc new file mode 100644 index 00000000000000..99003cce50ed27 --- /dev/null +++ b/src/node_blob.cc @@ -0,0 +1,323 @@ +#include "node_blob.h" +#include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "node_errors.h" +#include "threadpoolwork-inl.h" +#include "v8.h" + +#include + +namespace node { + +using v8::Array; +using v8::ArrayBuffer; +using v8::ArrayBufferView; +using v8::BackingStore; +using v8::Context; +using v8::EscapableHandleScope; +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::HandleScope; +using v8::Local; +using v8::MaybeLocal; +using v8::Number; +using v8::Object; +using v8::Uint32; +using v8::Undefined; +using v8::Value; + +void Blob::Initialize(Environment* env, v8::Local target) { + env->SetMethod(target, "createBlob", New); + FixedSizeBlobCopyJob::Initialize(env, target); +} + +Local Blob::GetConstructorTemplate(Environment* env) { + Local tmpl = env->blob_constructor_template(); + if (tmpl.IsEmpty()) { + tmpl = FunctionTemplate::New(env->isolate()); + tmpl->InstanceTemplate()->SetInternalFieldCount(1); + tmpl->Inherit(BaseObject::GetConstructorTemplate(env)); + tmpl->SetClassName( + FIXED_ONE_BYTE_STRING(env->isolate(), "Blob")); + env->SetProtoMethod(tmpl, "toArrayBuffer", ToArrayBuffer); + env->SetProtoMethod(tmpl, "slice", ToSlice); + env->set_blob_constructor_template(tmpl); + } + return tmpl; +} + +bool Blob::HasInstance(Environment* env, v8::Local object) { + return GetConstructorTemplate(env)->HasInstance(object); +} + +BaseObjectPtr Blob::Create( + Environment* env, + const std::vector store, + size_t length) { + + HandleScope scope(env->isolate()); + + Local ctor; + if (!GetConstructorTemplate(env)->GetFunction(env->context()).ToLocal(&ctor)) + return BaseObjectPtr(); + + Local obj; + if (!ctor->NewInstance(env->context()).ToLocal(&obj)) + return BaseObjectPtr(); + + return MakeBaseObject(env, obj, store, length); +} + +void Blob::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args[0]->IsArray()); // sources + CHECK(args[1]->IsUint32()); // length + + std::vector entries; + + size_t length = args[1].As()->Value(); + size_t len = 0; + Local ary = args[0].As(); + for (size_t n = 0; n < ary->Length(); n++) { + Local entry; + if (!ary->Get(env->context(), n).ToLocal(&entry)) + return; + CHECK(entry->IsArrayBufferView() || Blob::HasInstance(env, entry)); + if (entry->IsArrayBufferView()) { + Local view = entry.As(); + CHECK_EQ(view->ByteOffset(), 0); + std::shared_ptr store = view->Buffer()->GetBackingStore(); + size_t byte_length = view->ByteLength(); + view->Buffer()->Detach(); // The Blob will own the backing store now. + entries.emplace_back(BlobEntry{std::move(store), byte_length, 0}); + len += byte_length; + } else { + Blob* blob; + ASSIGN_OR_RETURN_UNWRAP(&blob, entry); + auto source = blob->entries(); + entries.insert(entries.end(), source.begin(), source.end()); + len += blob->length(); + } + } + CHECK_EQ(length, len); + + BaseObjectPtr blob = Create(env, entries, length); + if (blob) + args.GetReturnValue().Set(blob->object()); +} + +void Blob::ToArrayBuffer(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Blob* blob; + ASSIGN_OR_RETURN_UNWRAP(&blob, args.Holder()); + Local ret; + if (blob->GetArrayBuffer(env).ToLocal(&ret)) + args.GetReturnValue().Set(ret); +} + +void Blob::ToSlice(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Blob* blob; + ASSIGN_OR_RETURN_UNWRAP(&blob, args.Holder()); + CHECK(args[0]->IsUint32()); + CHECK(args[1]->IsUint32()); + size_t start = args[0].As()->Value(); + size_t end = args[1].As()->Value(); + BaseObjectPtr slice = blob->Slice(env, start, end); + if (slice) + args.GetReturnValue().Set(slice->object()); +} + +void Blob::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackFieldWithSize("store", length_); +} + +MaybeLocal Blob::GetArrayBuffer(Environment* env) { + EscapableHandleScope scope(env->isolate()); + size_t len = length(); + std::shared_ptr store = + ArrayBuffer::NewBackingStore(env->isolate(), len); + if (len > 0) { + unsigned char* dest = static_cast(store->Data()); + size_t total = 0; + for (const auto& entry : entries()) { + unsigned char* src = static_cast(entry.store->Data()); + src += entry.offset; + memcpy(dest, src, entry.length); + dest += entry.length; + total += entry.length; + CHECK_LE(total, len); + } + } + + return scope.Escape(ArrayBuffer::New(env->isolate(), store)); +} + +BaseObjectPtr Blob::Slice(Environment* env, size_t start, size_t end) { + CHECK_LE(start, length()); + CHECK_LE(end, length()); + CHECK_LE(start, end); + + std::vector slices; + size_t total = end - start; + size_t remaining = total; + + if (total == 0) return Create(env, slices, 0); + + for (const auto& entry : entries()) { + if (start + entry.offset > entry.store->ByteLength()) { + start -= entry.length; + continue; + } + + size_t offset = entry.offset + start; + size_t len = std::min(remaining, entry.store->ByteLength() - offset); + slices.emplace_back(BlobEntry{entry.store, len, offset}); + + remaining -= len; + start = 0; + + if (remaining == 0) + break; + } + + return Create(env, slices, total); +} + +Blob::Blob( + Environment* env, + v8::Local obj, + const std::vector& store, + size_t length) + : BaseObject(env, obj), + store_(store), + length_(length) { + MakeWeak(); +} + +BaseObjectPtr +Blob::BlobTransferData::Deserialize( + Environment* env, + Local context, + std::unique_ptr self) { + if (context != env->context()) { + THROW_ERR_MESSAGE_TARGET_CONTEXT_UNAVAILABLE(env); + return {}; + } + return Blob::Create(env, store_, length_); +} + +BaseObject::TransferMode Blob::GetTransferMode() const { + return BaseObject::TransferMode::kCloneable; +} + +std::unique_ptr Blob::CloneForMessaging() const { + return std::make_unique(store_, length_); +} + +FixedSizeBlobCopyJob::FixedSizeBlobCopyJob( + Environment* env, + Local object, + Blob* blob, + FixedSizeBlobCopyJob::Mode mode) + : AsyncWrap(env, object, AsyncWrap::PROVIDER_FIXEDSIZEBLOBCOPY), + ThreadPoolWork(env), + mode_(mode) { + if (mode == FixedSizeBlobCopyJob::Mode::SYNC) MakeWeak(); + source_ = blob->entries(); + length_ = blob->length(); +} + +void FixedSizeBlobCopyJob::AfterThreadPoolWork(int status) { + Environment* env = AsyncWrap::env(); + CHECK_EQ(mode_, Mode::ASYNC); + CHECK(status == 0 || status == UV_ECANCELED); + std::unique_ptr ptr(this); + HandleScope handle_scope(env->isolate()); + Context::Scope context_scope(env->context()); + Local args[2]; + + if (status == UV_ECANCELED) { + args[0] = Number::New(env->isolate(), status), + args[1] = Undefined(env->isolate()); + } else { + args[0] = Undefined(env->isolate()); + args[1] = ArrayBuffer::New(env->isolate(), destination_); + } + + ptr->MakeCallback(env->ondone_string(), arraysize(args), args); +} + +void FixedSizeBlobCopyJob::DoThreadPoolWork() { + Environment* env = AsyncWrap::env(); + destination_ = ArrayBuffer::NewBackingStore(env->isolate(), length_); + unsigned char* dest = static_cast(destination_->Data()); + if (length_ > 0) { + size_t total = 0; + for (const auto& entry : source_) { + unsigned char* src = static_cast(entry.store->Data()); + src += entry.offset; + memcpy(dest, src, entry.length); + dest += entry.length; + total += entry.length; + CHECK_LE(total, length_); + } + } +} + +void FixedSizeBlobCopyJob::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackFieldWithSize("source", length_); + tracker->TrackFieldWithSize( + "destination", + destination_ ? destination_->ByteLength() : 0); +} + +void FixedSizeBlobCopyJob::Initialize(Environment* env, Local target) { + v8::Local job = env->NewFunctionTemplate(New); + job->Inherit(AsyncWrap::GetConstructorTemplate(env)); + job->InstanceTemplate()->SetInternalFieldCount( + AsyncWrap::kInternalFieldCount); + env->SetProtoMethod(job, "run", Run); + env->SetConstructorFunction(target, "FixedSizeBlobCopyJob", job); +} + +void FixedSizeBlobCopyJob::New(const FunctionCallbackInfo& args) { + static constexpr size_t kMaxSyncLength = 4096; + static constexpr size_t kMaxEntryCount = 4; + + Environment* env = Environment::GetCurrent(args); + CHECK(args.IsConstructCall()); + CHECK(args[0]->IsObject()); + CHECK(Blob::HasInstance(env, args[0])); + + Blob* blob; + ASSIGN_OR_RETURN_UNWRAP(&blob, args[0]); + + // This is a fairly arbitrary heuristic. We want to avoid deferring to + // the threadpool if the amount of data being copied is small and there + // aren't that many entries to copy. + FixedSizeBlobCopyJob::Mode mode = + (blob->length() < kMaxSyncLength && + blob->entries().size() < kMaxEntryCount) ? + FixedSizeBlobCopyJob::Mode::SYNC : + FixedSizeBlobCopyJob::Mode::ASYNC; + + new FixedSizeBlobCopyJob(env, args.This(), blob, mode); +} + +void FixedSizeBlobCopyJob::Run(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + FixedSizeBlobCopyJob* job; + ASSIGN_OR_RETURN_UNWRAP(&job, args.Holder()); + if (job->mode() == FixedSizeBlobCopyJob::Mode::ASYNC) + return job->ScheduleWork(); + + job->DoThreadPoolWork(); + args.GetReturnValue().Set( + ArrayBuffer::New(env->isolate(), job->destination_)); +} + +} // namespace node diff --git a/src/node_blob.h b/src/node_blob.h new file mode 100644 index 00000000000000..d306253fdd1d31 --- /dev/null +++ b/src/node_blob.h @@ -0,0 +1,133 @@ +#ifndef SRC_NODE_BLOB_H_ +#define SRC_NODE_BLOB_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "async_wrap.h" +#include "base_object.h" +#include "env.h" +#include "memory_tracker.h" +#include "node_internals.h" +#include "node_worker.h" +#include "v8.h" + +#include + +namespace node { + +struct BlobEntry { + std::shared_ptr store; + size_t length; + size_t offset; +}; + +class Blob : public BaseObject { + public: + static void Initialize(Environment* env, v8::Local target); + + static void New(const v8::FunctionCallbackInfo& args); + static void ToArrayBuffer(const v8::FunctionCallbackInfo& args); + static void ToSlice(const v8::FunctionCallbackInfo& args); + + static v8::Local GetConstructorTemplate( + Environment* env); + + static BaseObjectPtr Create( + Environment* env, + const std::vector store, + size_t length); + + static bool HasInstance(Environment* env, v8::Local object); + + const std::vector entries() const { + return store_; + } + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Blob); + SET_SELF_SIZE(Blob); + + // Copies the contents of the Blob into an ArrayBuffer. + v8::MaybeLocal GetArrayBuffer(Environment* env); + + BaseObjectPtr Slice(Environment* env, size_t start, size_t end); + + inline size_t length() const { return length_; } + + class BlobTransferData : public worker::TransferData { + public: + explicit BlobTransferData( + const std::vector& store, + size_t length) + : store_(store), + length_(length) {} + + BaseObjectPtr Deserialize( + Environment* env, + v8::Local context, + std::unique_ptr self) override; + + SET_MEMORY_INFO_NAME(BlobTransferData) + SET_SELF_SIZE(BlobTransferData) + SET_NO_MEMORY_INFO() + + private: + std::vector store_; + size_t length_ = 0; + }; + + BaseObject::TransferMode GetTransferMode() const override; + std::unique_ptr CloneForMessaging() const override; + + Blob( + Environment* env, + v8::Local obj, + const std::vector& store, + size_t length); + + private: + std::vector store_; + size_t length_ = 0; +}; + +class FixedSizeBlobCopyJob : public AsyncWrap, public ThreadPoolWork { + public: + enum class Mode { + SYNC, + ASYNC + }; + + static void Initialize(Environment* env, v8::Local target); + static void New(const v8::FunctionCallbackInfo& args); + static void Run(const v8::FunctionCallbackInfo& args); + + bool IsNotIndicativeOfMemoryLeakAtExit() const override { + return true; + } + + void DoThreadPoolWork() override; + void AfterThreadPoolWork(int status) override; + + Mode mode() const { return mode_; } + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(FixedSizeBlobCopyJob) + SET_SELF_SIZE(FixedSizeBlobCopyJob) + + private: + FixedSizeBlobCopyJob( + Environment* env, + v8::Local object, + Blob* blob, + Mode mode = Mode::ASYNC); + + Mode mode_; + std::vector source_; + std::shared_ptr destination_; + size_t length_ = 0; +}; + +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_NODE_BLOB_H_ diff --git a/src/node_buffer.cc b/src/node_buffer.cc index 455c2b4949d23b..6a66bd833ae345 100644 --- a/src/node_buffer.cc +++ b/src/node_buffer.cc @@ -22,6 +22,7 @@ #include "node_buffer.h" #include "allocated_buffer-inl.h" #include "node.h" +#include "node_blob.h" #include "node_errors.h" #include "node_internals.h" @@ -1177,6 +1178,8 @@ void Initialize(Local target, env->SetMethod(target, "ucs2Write", StringWrite); env->SetMethod(target, "utf8Write", StringWrite); + Blob::Initialize(env, target); + // It can be a nullptr when running inside an isolate where we // do not own the ArrayBuffer allocator. if (NodeArrayBufferAllocator* allocator = diff --git a/test/parallel/test-blob.js b/test/parallel/test-blob.js new file mode 100644 index 00000000000000..b32cacdda723a9 --- /dev/null +++ b/test/parallel/test-blob.js @@ -0,0 +1,187 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Blob } = require('buffer'); +const { MessageChannel } = require('worker_threads'); + +{ + const b = new Blob(); + assert.strictEqual(b.size, 0); + assert.strictEqual(b.type, ''); +} + +assert.throws(() => new Blob(false), { + code: 'ERR_INVALID_ARG_TYPE' +}); + +assert.throws(() => new Blob('hello'), { + code: 'ERR_INVALID_ARG_TYPE' +}); + +assert.throws(() => new Blob({}), { + code: 'ERR_INVALID_ARG_TYPE' +}); + +assert.throws(() => new Blob(['test', 1]), { + code: 'ERR_INVALID_ARG_TYPE' +}); + +{ + const b = new Blob([]); + assert(b); + assert.strictEqual(b.size, 0); + assert.strictEqual(b.type, ''); + + b.arrayBuffer().then(common.mustCall((ab) => { + assert.deepStrictEqual(ab, new ArrayBuffer(0)); + })); + b.text().then(common.mustCall((text) => { + assert.strictEqual(text, ''); + })); + const c = b.slice(); + assert.strictEqual(c.size, 0); +} + +{ + assert.throws(() => new Blob([], { type: 1 }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => new Blob([], { type: false }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => new Blob([], { type: {} }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +} + +{ + const b = new Blob(['616263'], { encoding: 'hex', type: 'foo' }); + assert.strictEqual(b.size, 3); + assert.strictEqual(b.type, 'foo'); + b.text().then(common.mustCall((text) => { + assert.strictEqual(text, 'abc'); + })); +} + +{ + const b = new Blob([Buffer.from('abc')]); + assert.strictEqual(b.size, 3); + b.text().then(common.mustCall((text) => { + assert.strictEqual(text, 'abc'); + })); +} + +{ + const b = new Blob([new ArrayBuffer(3)]); + assert.strictEqual(b.size, 3); + b.text().then(common.mustCall((text) => { + assert.strictEqual(text, '\0\0\0'); + })); +} + +{ + const b = new Blob([new Uint8Array(3)]); + assert.strictEqual(b.size, 3); + b.text().then(common.mustCall((text) => { + assert.strictEqual(text, '\0\0\0'); + })); +} + +{ + const b = new Blob([new Blob(['abc'])]); + assert.strictEqual(b.size, 3); + b.text().then(common.mustCall((text) => { + assert.strictEqual(text, 'abc'); + })); +} + +{ + const b = new Blob(['hello', Buffer.from('world')]); + assert.strictEqual(b.size, 10); + b.text().then(common.mustCall((text) => { + assert.strictEqual(text, 'helloworld'); + })); +} + +{ + const b = new Blob( + [ + 'h', + 'e', + 'l', + 'lo', + Buffer.from('world') + ]); + assert.strictEqual(b.size, 10); + b.text().then(common.mustCall((text) => { + assert.strictEqual(text, 'helloworld'); + })); +} + +{ + const b = new Blob(['hello', Buffer.from('world')]); + assert.strictEqual(b.size, 10); + assert.strictEqual(b.type, ''); + + const c = b.slice(1, -1, 'foo'); + assert.strictEqual(c.type, 'foo'); + c.text().then(common.mustCall((text) => { + assert.strictEqual(text, 'elloworl'); + })); + + const d = c.slice(1, -1); + d.text().then(common.mustCall((text) => { + assert.strictEqual(text, 'llowor'); + })); + + const e = d.slice(1, -1); + e.text().then(common.mustCall((text) => { + assert.strictEqual(text, 'lowo'); + })); + + const f = e.slice(1, -1); + f.text().then(common.mustCall((text) => { + assert.strictEqual(text, 'ow'); + })); + + const g = f.slice(1, -1); + assert.strictEqual(g.type, 'foo'); + g.text().then(common.mustCall((text) => { + assert.strictEqual(text, ''); + })); + + assert.strictEqual(b.size, 10); + assert.strictEqual(b.type, ''); + + assert.throws(() => b.slice(-1, 1), { + code: 'ERR_OUT_OF_RANGE' + }); + assert.throws(() => b.slice(1, 100), { + code: 'ERR_OUT_OF_RANGE' + }); + + assert.throws(() => b.slice(1, 2, false), { + code: 'ERR_INVALID_ARG_TYPE' + }); +} + +{ + const b = new Blob([Buffer.from('hello'), Buffer.from('world')]); + const mc = new MessageChannel(); + mc.port1.onmessage = common.mustCall(({ data }) => { + data.text().then(common.mustCall((text) => { + assert.strictEqual(text, 'helloworld'); + })); + mc.port1.close(); + }); + mc.port2.postMessage(b); + b.text().then(common.mustCall((text) => { + assert.strictEqual(text, 'helloworld'); + })); +} + +{ + const b = new Blob(['hello'], { type: '\x01' }); + assert.strictEqual(b.type, ''); +} diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 0c3618edca00ba..e38c583511b58e 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -86,6 +86,7 @@ const expectedModules = new Set([ 'NativeModule internal/validators', 'NativeModule internal/vm/module', 'NativeModule internal/worker/js_transferable', + 'NativeModule internal/blob', 'NativeModule path', 'NativeModule timers', 'NativeModule url', diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js index 957c7f7440a4bc..921fe8fc4bdc86 100644 --- a/test/sequential/test-async-wrap-getasyncid.js +++ b/test/sequential/test-async-wrap-getasyncid.js @@ -54,6 +54,7 @@ const { getSystemErrorName } = require('util'); delete providers.ELDHISTOGRAM; delete providers.SIGINTWATCHDOG; delete providers.WORKERHEAPSNAPSHOT; + delete providers.FIXEDSIZEBLOBCOPY; const objKeys = Object.keys(providers); if (objKeys.length > 0) diff --git a/tools/doc/type-parser.mjs b/tools/doc/type-parser.mjs index e49f6898bba3eb..3feac3b211c749 100644 --- a/tools/doc/type-parser.mjs +++ b/tools/doc/type-parser.mjs @@ -38,6 +38,8 @@ const customTypesMap = { 'WebAssembly.Instance': `${jsDocPrefix}Reference/Global_Objects/WebAssembly/Instance`, + 'Blob': 'buffer.html#buffer_class_blob', + 'Iterable': `${jsDocPrefix}Reference/Iteration_protocols#The_iterable_protocol`, 'Iterator':