diff --git a/src/workerd/api/blob.c++ b/src/workerd/api/blob.c++ index c79556e44c3..276f74191c2 100644 --- a/src/workerd/api/blob.c++ +++ b/src/workerd/api/blob.c++ @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -14,19 +15,27 @@ namespace workerd::api { namespace { // Concatenate an array of segments (parameter to Blob constructor). -jsg::BufferSource concat(jsg::Lock& js, jsg::Optional maybeBits) { - // TODO(perf): Make it so that a Blob can keep references to the input data rather than copy it. - // Note that we can't keep references to ArrayBuffers since they are mutable, but we can - // reference other Blobs in the input. - +kj::Maybe concat(jsg::Lock& js, jsg::Optional maybeBits) { auto bits = kj::mv(maybeBits).orDefault(nullptr); + if (bits.size() == 0) { + return kj::none; + } + auto rejectResizable = FeatureFlags::get(js).getNoResizableArrayBufferInBlob(); auto maxBlobSize = Worker::Isolate::from(js).getLimitEnforcer().getBlobSizeLimit(); + static constexpr int kMaxInt KJ_UNUSED = kj::maxValue; + KJ_DASSERT(maxBlobSize <= kMaxInt, "Blob size limit exceeds int range"); size_t size = 0; + kj::SmallArray cachedPartSizes(bits.size()); + int index = 0; for (auto& part: bits) { size_t partSize = 0; KJ_SWITCH_ONEOF(part) { - KJ_CASE_ONEOF(bytes, kj::Array) { + KJ_CASE_ONEOF(bytes, jsg::JsBufferSource) { + if (rejectResizable) { + JSG_REQUIRE( + !bytes.isResizable(), TypeError, "Cannot create a Blob with a resizable ArrayBuffer"); + } partSize = bytes.size(); } KJ_CASE_ONEOF(text, kj::String) { @@ -36,6 +45,7 @@ jsg::BufferSource concat(jsg::Lock& js, jsg::Optional maybeBits) { partSize = blob->getData().size(); } } + cachedPartSizes[index++] = partSize; // We can skip the remaining checks if the part is empty. if (partSize == 0) continue; @@ -57,25 +67,34 @@ jsg::BufferSource concat(jsg::Lock& js, jsg::Optional maybeBits) { size += partSize; } - auto backing = jsg::BackingStore::alloc(js, size); - auto result = jsg::BufferSource(js, kj::mv(backing)); + if (size == 0) { + return kj::none; + } - if (size == 0) return kj::mv(result); + auto u8 = jsg::JsUint8Array::create(js, size); - auto view = result.asArrayPtr(); + auto view = u8.asArrayPtr(); + index = 0; for (auto& part: bits) { KJ_SWITCH_ONEOF(part) { - KJ_CASE_ONEOF(bytes, kj::Array) { - if (bytes.size() == 0) continue; - KJ_ASSERT(view.size() >= bytes.size()); - view.first(bytes.size()).copyFrom(bytes); - view = view.slice(bytes.size()); + KJ_CASE_ONEOF(bytes, jsg::JsBufferSource) { + size_t cachedSize = cachedPartSizes[index++]; + // If the ArrayBuffer was resized larger, we'll ignore the additional bytes. + // If the ArrayBuffer was resized smaller, we'll copy up the current size + // and the remaining bytes for this chunk will be left as zeros. + size_t toCopy = kj::min(bytes.size(), cachedSize); + if (toCopy > 0) { + KJ_ASSERT(view.size() >= toCopy); + view.first(toCopy).copyFrom(bytes.asArrayPtr().first(toCopy)); + } + view = view.slice(cachedSize); } KJ_CASE_ONEOF(text, kj::String) { auto byteLength = text.asBytes().size(); if (byteLength == 0) continue; KJ_ASSERT(view.size() >= byteLength); + KJ_ASSERT(byteLength == cachedPartSizes[index++]); view.first(byteLength).copyFrom(text.asBytes()); view = view.slice(byteLength); } @@ -83,6 +102,7 @@ jsg::BufferSource concat(jsg::Lock& js, jsg::Optional maybeBits) { auto data = blob->getData(); if (data.size() == 0) continue; KJ_ASSERT(view.size() >= data.size()); + KJ_ASSERT(data.size() == cachedPartSizes[index++]); view.first(data.size()).copyFrom(data); view = view.slice(data.size()); } @@ -91,7 +111,7 @@ jsg::BufferSource concat(jsg::Lock& js, jsg::Optional maybeBits) { KJ_ASSERT(view == nullptr); - return kj::mv(result); + return jsg::JsBufferSource(u8); } kj::String normalizeType(kj::String type) { @@ -117,15 +137,22 @@ kj::String normalizeType(kj::String type) { } // namespace +Blob::Blob(kj::String type): ownData(Empty{}), data(nullptr), type(kj::mv(type)) {} + Blob::Blob(kj::Array data, kj::String type) : ownData(kj::mv(data)), data(ownData.get>()), type(kj::mv(type)) {} -Blob::Blob(jsg::Lock& js, jsg::BufferSource data, kj::String type) - : ownData(kj::mv(data)), - data(ownData.get().asArrayPtr()), - type(kj::mv(type)) {} +Blob::Blob(jsg::Lock& js, jsg::JsBufferSource data, kj::String type) + : ownData(data.addRef(js)), + data(data.asArrayPtr()), + type(kj::mv(type)) { + if (FeatureFlags::get(js).getNoResizableArrayBufferInBlob()) { + JSG_REQUIRE( + !data.isResizable(), TypeError, "Cannot create a Blob with a resizable ArrayBuffer"); + } +} Blob::Blob(jsg::Ref parent, kj::ArrayPtr data, kj::String type) : ownData(kj::mv(parent)), @@ -141,11 +168,30 @@ jsg::Ref Blob::constructor( } } - return js.alloc(js, concat(js, kj::mv(bits)), kj::mv(type)); + KJ_IF_SOME(b, bits) { + // Optimize for the case where the input is a single Blob, where we can just + // return a new view on the existing data without copying. + if (b.size() == 1) { + KJ_IF_SOME(parent, b[0].template tryGet>()) { + if (parent->getSize() == 0) { + return js.alloc(kj::mv(type)); + } + auto ptr = parent->data; + KJ_IF_SOME(root, parent->ownData.template tryGet>()) { + parent = root.addRef(); + } + return js.alloc(kj::mv(parent), ptr, kj::mv(type)); + } + } + } + + KJ_IF_SOME(data, concat(js, kj::mv(bits))) { + return js.alloc(js, data, kj::mv(type)); + } + return js.alloc(kj::mv(type)); } kj::ArrayPtr Blob::getData() const { - FeatureObserver::maybeRecordUse(FeatureObserver::Feature::BLOB_GET_DATA); return data; } @@ -153,6 +199,13 @@ jsg::Ref Blob::slice(jsg::Lock& js, jsg::Optional maybeStart, jsg::Optional maybeEnd, jsg::Optional type) { + + auto normalizedType = normalizeType(kj::mv(type).orDefault(nullptr)); + if (data.size() == 0) { + // Blob is empty, there's nothing to slice. + return js.alloc(kj::mv(normalizedType)); + } + int start = maybeStart.orDefault(0); int end = maybeEnd.orDefault(data.size()); @@ -160,50 +213,65 @@ jsg::Ref Blob::slice(jsg::Lock& js, // Negative value interpreted as offset from end. start += data.size(); } - // Clamp start to range. - if (start < 0) { - start = 0; - } else if (start > data.size()) { - start = data.size(); - } - if (end < 0) { // Negative value interpreted as offset from end. end += data.size(); } - // Clamp end to range. - if (end < start) { - end = start; - } else if (end > data.size()) { - end = data.size(); + + // Clamp start and end to range. + start = kj::max(0, kj::min(start, static_cast(data.size()))); + end = kj::max(start, kj::min(end, static_cast(data.size()))); + + // We run with KJ_IREQUIRE checks enabled in production, which will catch + // out of bounds start/end ... but since we're clamping them above, this + // should never actually be a problem. + auto slicedData = data.slice(start, end); + + // If the slice is empty, we can just return a new empty Blob without worrying about + // referencing the original data at all. Super minor optimization that avoids an + // unnecessary refcount. + if (slicedData.size() == 0) { + return js.alloc(kj::mv(normalizedType)); } - return js.alloc( - JSG_THIS, data.slice(start, end), normalizeType(kj::mv(type).orDefault(nullptr))); + KJ_SWITCH_ONEOF(ownData) { + KJ_CASE_ONEOF(_, Empty) { + // Handled at the beginning of the function with the zero-length check. + KJ_FAIL_ASSERT("Empty blob should have been handled at the beginning of the function"); + } + KJ_CASE_ONEOF(parent, jsg::Ref) { + // If this blob is itself a slice (backed by a Ref), reference the + // root data-owning blob directly. This prevents unbounded chain depth — + // every slice always points to the root, so depth is always ≤ 1. + return js.alloc(parent.addRef(), slicedData, kj::mv(normalizedType)); + } + KJ_CASE_ONEOF(_, jsg::JsRef) { + return js.alloc(JSG_THIS, slicedData, kj::mv(normalizedType)); + } + KJ_CASE_ONEOF(_, kj::Array) { + return js.alloc(JSG_THIS, slicedData, kj::mv(normalizedType)); + } + } + KJ_UNREACHABLE; } -jsg::Promise Blob::arrayBuffer(jsg::Lock& js) { +jsg::Promise> Blob::arrayBuffer(jsg::Lock& js) { FeatureObserver::maybeRecordUse(FeatureObserver::Feature::BLOB_AS_ARRAY_BUFFER); - // We use BufferSource here instead of kj::Array to ensure that the - // resulting backing store is associated with the isolate, which is necessary - // for when we start making use of v8 sandboxing. - auto backing = jsg::BackingStore::alloc(js, data.size()); - backing.asArrayPtr().copyFrom(data); - return js.resolvedPromise(jsg::BufferSource(js, kj::mv(backing))); + auto ret = jsg::JsArrayBuffer::create(js, data); + return js.resolvedPromise(ret.addRef(js)); } -jsg::Promise Blob::bytes(jsg::Lock& js) { - // We use BufferSource here instead of kj::Array to ensure that the - // resulting backing store is associated with the isolate, which is necessary - // for when we start making use of v8 sandboxing. - auto backing = jsg::BackingStore::alloc(js, data.size()); - backing.asArrayPtr().copyFrom(data); - return js.resolvedPromise(jsg::BufferSource(js, kj::mv(backing))); +jsg::Promise> Blob::bytes(jsg::Lock& js) { + FeatureObserver::maybeRecordUse(FeatureObserver::Feature::BLOB_AS_ARRAY_BUFFER); + auto ret = jsg::JsUint8Array::create(js, data); + return js.resolvedPromise(ret.addRef(js)); } -jsg::Promise Blob::text(jsg::Lock& js) { +jsg::Promise> Blob::text(jsg::Lock& js) { FeatureObserver::maybeRecordUse(FeatureObserver::Feature::BLOB_AS_TEXT); - return js.resolvedPromise(kj::str(data.asChars())); + // Using js.str here instead of returning kj::String avoids an additional + // intermediate allocation and copy of the string data. + return js.resolvedPromise(js.str(data.asChars()).addRef(js)); } jsg::Ref Blob::stream(jsg::Lock& js) { @@ -214,13 +282,18 @@ jsg::Ref Blob::stream(jsg::Lock& js) { // ======================================================================================= +File::File(kj::String name, kj::String type, double lastModified) + : Blob(kj::mv(type)), + name(kj::mv(name)), + lastModified(lastModified) {} + File::File(kj::Array data, kj::String name, kj::String type, double lastModified) : Blob(kj::mv(data), kj::mv(type)), name(kj::mv(name)), lastModified(lastModified) {} File::File( - jsg::Lock& js, jsg::BufferSource data, kj::String name, kj::String type, double lastModified) + jsg::Lock& js, jsg::JsBufferSource data, kj::String name, kj::String type, double lastModified) : Blob(js, kj::mv(data), kj::mv(type)), name(kj::mv(name)), lastModified(lastModified) {} @@ -252,7 +325,27 @@ jsg::Ref File::constructor( lastModified = dateNow(); } - return js.alloc(js, concat(js, kj::mv(bits)), kj::mv(name), kj::mv(type), lastModified); + KJ_IF_SOME(b, bits) { + // Optimize for the case where the input is a single Blob, where we can just + // return a new view on the existing data without copying. + if (b.size() == 1) { + KJ_IF_SOME(parent, b[0].template tryGet>()) { + if (parent->getSize() == 0) { + return js.alloc(kj::mv(name), kj::mv(type), lastModified); + } + auto ptr = parent->data; + KJ_IF_SOME(root, parent->ownData.template tryGet>()) { + parent = root.addRef(); + } + return js.alloc(kj::mv(parent), ptr, kj::mv(name), kj::mv(type), lastModified); + } + } + } + + KJ_IF_SOME(data, concat(js, kj::mv(bits))) { + return js.alloc(js, data, kj::mv(name), kj::mv(type), lastModified); + } + return js.alloc(kj::mv(name), kj::mv(type), lastModified); } } // namespace workerd::api diff --git a/src/workerd/api/blob.h b/src/workerd/api/blob.h index 7ce430c978f..8fb37852569 100644 --- a/src/workerd/api/blob.h +++ b/src/workerd/api/blob.h @@ -15,7 +15,9 @@ class File; // An implementation of the Web Platform Standard Blob API class Blob: public jsg::Object { public: - Blob(jsg::Lock& js, jsg::BufferSource data, kj::String type); + // Creates an empty Blob + Blob(kj::String type); + Blob(jsg::Lock& js, jsg::JsBufferSource data, kj::String type); Blob(jsg::Ref parent, kj::ArrayPtr data, kj::String type); kj::ArrayPtr getData() const KJ_LIFETIMEBOUND; @@ -30,7 +32,8 @@ class Blob: public jsg::Object { JSG_STRUCT(type, endings); }; - using Bits = kj::Array, kj::String, jsg::Ref>>; + using BitsValue = kj::OneOf>; + using Bits = kj::Array; static jsg::Ref constructor( jsg::Lock& js, jsg::Optional bits, jsg::Optional options); @@ -38,7 +41,7 @@ class Blob: public jsg::Object { int getSize() const { return data.size(); } - kj::StringPtr getType() const { + kj::StringPtr getType() const KJ_LIFETIMEBOUND { return type; } @@ -47,9 +50,11 @@ class Blob: public jsg::Object { jsg::Optional end, jsg::Optional type); - jsg::Promise arrayBuffer(jsg::Lock& js); - jsg::Promise bytes(jsg::Lock& js); - jsg::Promise text(jsg::Lock& js); + // Each of the consumption methods (arrayBuffer, bytes, text) create copies of + // the Blob's underlying data. + jsg::Promise> arrayBuffer(jsg::Lock& js); + jsg::Promise> bytes(jsg::Lock& js); + jsg::Promise> text(jsg::Lock& js); jsg::Ref stream(jsg::Lock& js); JSG_RESOURCE_TYPE(Blob, CompatibilityFlags::Reader flags) { @@ -75,7 +80,8 @@ class Blob: public jsg::Object { void visitForMemoryInfo(jsg::MemoryTracker& tracker) const { KJ_SWITCH_ONEOF(ownData) { - KJ_CASE_ONEOF(data, jsg::BufferSource) { + KJ_CASE_ONEOF(_, Empty) {} + KJ_CASE_ONEOF(data, jsg::JsRef) { tracker.trackField("ownData", data); } KJ_CASE_ONEOF(data, jsg::Ref) { @@ -91,18 +97,22 @@ class Blob: public jsg::Object { private: Blob(kj::Array data, kj::String type); - // Using a jsg::BufferSource to store the ownData allows the associated isolate + // Sentinel type for the case where the Blob is just ... empty. + struct Empty {}; + + // Using a jsg::JsRef to store the ownData allows the associated isolate // to track the external data allocation correctly. // The Variation that uses kj::Array only is used only in very // specific cases (i.e. the internal fiddle service) where we parse FormData // outside of the isolate lock. - kj::OneOf, jsg::Ref> ownData; + kj::OneOf, kj::Array, jsg::Ref> ownData; kj::ArrayPtr data; kj::String type; void visitForGc(jsg::GcVisitor& visitor) { KJ_SWITCH_ONEOF(ownData) { - KJ_CASE_ONEOF(b, jsg::BufferSource) { + KJ_CASE_ONEOF(_, Empty) {} + KJ_CASE_ONEOF(b, jsg::JsRef) { visitor.visit(b); } KJ_CASE_ONEOF(b, jsg::Ref) { @@ -119,12 +129,17 @@ class Blob: public jsg::Object { // An implementation of the Web Platform Standard File API class File: public Blob { public: + // Creates a zero-length File + File(kj::String name, kj::String type, double lastModified); // This constructor variation is used when a File is created outside of the isolate // lock. This is currently only the case when parsing FormData outside of running // JavaScript (such as in the internal fiddle service). File(kj::Array data, kj::String name, kj::String type, double lastModified); - File( - jsg::Lock& js, jsg::BufferSource data, kj::String name, kj::String type, double lastModified); + File(jsg::Lock& js, + jsg::JsBufferSource data, + kj::String name, + kj::String type, + double lastModified); File(jsg::Ref parent, kj::ArrayPtr data, kj::String name, diff --git a/src/workerd/api/filesystem.c++ b/src/workerd/api/filesystem.c++ index 9db768d04bc..42006199e2f 100644 --- a/src/workerd/api/filesystem.c++ +++ b/src/workerd/api/filesystem.c++ @@ -1891,7 +1891,8 @@ jsg::Ref FileSystemModule::openAsBlob( KJ_CASE_ONEOF(file, kj::Rc) { KJ_SWITCH_ONEOF(file->readAllBytes(js)) { KJ_CASE_ONEOF(bytes, jsg::BufferSource) { - return js.alloc(js, kj::mv(bytes), kj::mv(options.type).orDefault(kj::String())); + return js.alloc( + js, bytes.getJsHandle(js), kj::mv(options.type).orDefault(kj::String())); } KJ_CASE_ONEOF(err, workerd::FsError) { throwFsError(js, err, "open"_kj); @@ -2558,7 +2559,7 @@ jsg::Promise> FileSystemFileHandle::getFile( KJ_SWITCH_ONEOF(file->readAllBytes(js)) { KJ_CASE_ONEOF(bytes, jsg::BufferSource) { return js.resolvedPromise( - js.alloc(js, kj::mv(bytes), jsg::USVString(kj::str(getName(js))), + js.alloc(js, bytes.getJsHandle(js), jsg::USVString(kj::str(getName(js))), kj::String(), (stat.lastModified - kj::UNIX_EPOCH) / kj::MILLISECONDS)); } KJ_CASE_ONEOF(err, workerd::FsError) { diff --git a/src/workerd/api/form-data.c++ b/src/workerd/api/form-data.c++ index 0148d742707..861de322b63 100644 --- a/src/workerd/api/form-data.c++ +++ b/src/workerd/api/form-data.c++ @@ -324,11 +324,9 @@ void FormData::parse(jsg::Lock& js, .value = kj::str(kj::mv(messageData)), }); } else { - auto backing = jsg::BackingStore::alloc(js, message.size()); - jsg::BufferSource bytes(js, kj::mv(backing)); - bytes.asArrayPtr().copyFrom(message); + auto bytes = jsg::JsArrayBuffer::create(js, message); data.add(FormData::Entry{.name = kj::str(name), - .value = js.alloc(js, kj::mv(bytes), kj::str(filename), + .value = js.alloc(js, jsg::JsBufferSource(bytes), kj::str(filename), kj::str(maybeType.orDefault(nullptr)), dateNow())}); } } else { diff --git a/src/workerd/api/http.c++ b/src/workerd/api/http.c++ index 8232f682864..7af3188005a 100644 --- a/src/workerd/api/http.c++ +++ b/src/workerd/api/http.c++ @@ -341,7 +341,7 @@ jsg::Promise> Body::blob(jsg::Lock& js) { }).orDefault(nullptr); } - return js.alloc(js, kj::mv(buffer), kj::mv(contentType)); + return js.alloc(js, buffer.getJsHandle(js), kj::mv(contentType)); }); } diff --git a/src/workerd/api/r2-bucket.c++ b/src/workerd/api/r2-bucket.c++ index 55f3dc769e9..311d114a159 100644 --- a/src/workerd/api/r2-bucket.c++ +++ b/src/workerd/api/r2-bucket.c++ @@ -1421,7 +1421,7 @@ jsg::Promise> R2Bucket::GetResult::blob(jsg::Lock& js) { // httpMetadata can't be null because GetResult always populates it. kj::String contentType = mapCopyString(KJ_REQUIRE_NONNULL(httpMetadata).contentType).orDefault(nullptr); - return js.alloc(js, kj::mv(buffer), kj::mv(contentType)); + return js.alloc(js, buffer.getJsHandle(js), kj::mv(contentType)); }); } diff --git a/src/workerd/api/tests/blob-test.js b/src/workerd/api/tests/blob-test.js index 9eac63b3a1d..2f1534ac00b 100644 --- a/src/workerd/api/tests/blob-test.js +++ b/src/workerd/api/tests/blob-test.js @@ -181,3 +181,20 @@ export const overLarge = { ); }, }; + +export const depthTest = { + async test() { + let blob = new Blob(['x']); + const depth = 100_000; + + for (let i = 0; i < depth; i++) { + blob = blob.slice(0, 1); + } + + // Force GC to trigger recursive traversal + gc(); + + // Access the blob to ensure it's still valid + console.log('Blob size:', blob.size); + }, +}; diff --git a/src/workerd/api/tests/blob-test.wd-test b/src/workerd/api/tests/blob-test.wd-test index a4a3e63a29d..11bd25dba2f 100644 --- a/src/workerd/api/tests/blob-test.wd-test +++ b/src/workerd/api/tests/blob-test.wd-test @@ -1,13 +1,18 @@ using Workerd = import "/workerd/workerd.capnp"; const unitTests :Workerd.Config = ( + v8Flags = ["--expose-gc"], services = [ ( name = "blob-test", worker = ( modules = [ (name = "worker", esModule = embed "blob-test.js") ], - compatibilityFlags = ["nodejs_compat", "set_tostring_tag", "workers_api_getters_setters_on_prototype"], + compatibilityFlags = [ + "nodejs_compat", + "set_tostring_tag", + "workers_api_getters_setters_on_prototype", + "resizable_array_buffer_in_blob"], bindings = [ (name = "request-blob", service = "blob-test") ] diff --git a/src/workerd/api/tests/blob2-test.js b/src/workerd/api/tests/blob2-test.js index 5149e2dd82a..9c30c134968 100644 --- a/src/workerd/api/tests/blob2-test.js +++ b/src/workerd/api/tests/blob2-test.js @@ -2,7 +2,7 @@ // Licensed under the Apache 2.0 license found in the LICENSE file or at: // https://opensource.org/licenses/Apache-2.0 -import { ok, strictEqual, deepStrictEqual } from 'node:assert'; +import { ok, strictEqual, deepStrictEqual, throws } from 'node:assert'; export const test = { async test() { @@ -36,3 +36,13 @@ export const bytes = { ok(u8_2 instanceof Uint8Array); }, }; + +export const noResizable = { + test() { + const ab = new ArrayBuffer(8, { maxByteLength: 16 }); + throws(() => new Blob([ab]), { + name: 'TypeError', + message: 'Cannot create a Blob with a resizable ArrayBuffer', + }); + }, +}; diff --git a/src/workerd/api/tests/blob2-test.wd-test b/src/workerd/api/tests/blob2-test.wd-test index c1d59d3df0a..410d51467ea 100644 --- a/src/workerd/api/tests/blob2-test.wd-test +++ b/src/workerd/api/tests/blob2-test.wd-test @@ -7,7 +7,11 @@ const unitTests :Workerd.Config = ( modules = [ (name = "worker", esModule = embed "blob2-test.js") ], - compatibilityFlags = ["nodejs_compat", "blob_standard_mime_type"], + compatibilityFlags = [ + "nodejs_compat", + "blob_standard_mime_type", + "no_resizable_array_buffer_in_blob", + ], ) ), ], diff --git a/src/workerd/api/web-socket.c++ b/src/workerd/api/web-socket.c++ index 338ab80ea32..fdb891037a9 100644 --- a/src/workerd/api/web-socket.c++ +++ b/src/workerd/api/web-socket.c++ @@ -1051,8 +1051,8 @@ kj::Promise> WebSocket::readLoop( KJ_CASE_ONEOF(data, kj::Array) { if (binaryType_ == BinaryType::BLOB) { // Per the WHATWG spec, deliver binary messages as Blob when binaryType is "blob". - auto bufferSource = jsg::BufferSource(js, jsg::BackingStore::from(js, kj::mv(data))); - auto blob = js.alloc(js, kj::mv(bufferSource), kj::str()); + auto ab = jsg::JsArrayBuffer::create(js, data); + auto blob = js.alloc(js, jsg::JsBufferSource(ab), kj::str()); dispatchEventImpl(js, js.alloc(js, kj::str("message"), kj::mv(blob))); } else { auto ab = js.arrayBuffer(kj::mv(data)).getHandle(js); diff --git a/src/workerd/io/compatibility-date.capnp b/src/workerd/io/compatibility-date.capnp index b09462250d9..d3998c0794b 100644 --- a/src/workerd/io/compatibility-date.capnp +++ b/src/workerd/io/compatibility-date.capnp @@ -1502,4 +1502,10 @@ struct CompatibilityFlags @0x8f8c1b68151b6cef { # When paired with `enable_version_api`, also exposes `ctx.version.metadata`. This is a separate # flag as we haven't decided on the exact behaviour of `ctx.version.metadata`, but the rest of # `ctx.version` is much more well defined. The behaviour of this flag will change in the future. + + noResizableArrayBufferInBlob @173 :Bool + $compatEnableFlag("no_resizable_array_buffer_in_blob") + $compatDisableFlag("resizable_array_buffer_in_blob"); + # When enabled, creating a Blob with a resizable ArrayBuffer will throw a TypeError, matching + # expected spec behavior. } diff --git a/src/workerd/jsg/buffersource.c++ b/src/workerd/jsg/buffersource.c++ index c59c68e9bbd..987e28c452a 100644 --- a/src/workerd/jsg/buffersource.c++ +++ b/src/workerd/jsg/buffersource.c++ @@ -135,6 +135,18 @@ v8::Local BufferSource::getHandle(Lock& js) { return handle.getHandle(js); } +JsBufferSource BufferSource::getJsHandle(Lock& js) { + auto handle = getHandle(js); + if (handle->IsArrayBuffer()) { + return JsBufferSource(JsArrayBuffer(handle.As())); + } else if (handle->IsSharedArrayBuffer()) { + return JsBufferSource(handle.As()); + } else if (handle->IsArrayBufferView()) { + return JsBufferSource(JsArrayBufferView(handle.As())); + } + KJ_UNREACHABLE; +} + void BufferSource::setDetachKey(Lock& js, v8::Local key) { auto handle = getHandle(js); auto buffer = handle->IsArrayBuffer() ? handle.As() diff --git a/src/workerd/jsg/buffersource.h b/src/workerd/jsg/buffersource.h index 9d711d4ec82..540502a8a21 100644 --- a/src/workerd/jsg/buffersource.h +++ b/src/workerd/jsg/buffersource.h @@ -343,6 +343,7 @@ class BufferSource { BackingStore detach(Lock& js, kj::Maybe> maybeKey = kj::none); v8::Local getHandle(Lock& js); + JsBufferSource getJsHandle(Lock& js); template inline kj::ArrayPtr asArrayPtr() KJ_LIFETIMEBOUND { diff --git a/src/workerd/jsg/jsvalue.c++ b/src/workerd/jsg/jsvalue.c++ index 3c9b3c93d57..08cd1bc57db 100644 --- a/src/workerd/jsg/jsvalue.c++ +++ b/src/workerd/jsg/jsvalue.c++ @@ -754,6 +754,9 @@ kj::ArrayPtr JsBufferSource::asArrayPtr() { return nullptr; } return kj::ArrayPtr(static_cast(buf->Data()), buf->ByteLength()); + } else if (inner->IsSharedArrayBuffer()) { + auto buf = inner.As(); + return kj::ArrayPtr(static_cast(buf->Data()), buf->ByteLength()); } else { KJ_DASSERT(inner->IsArrayBufferView()); auto view = inner.As(); @@ -774,6 +777,9 @@ size_t JsBufferSource::size() const { return 0; } return buf->ByteLength(); + } else if (inner->IsSharedArrayBuffer()) { + auto buf = inner.As(); + return buf->ByteLength(); } else { KJ_DASSERT(inner->IsArrayBufferView()); auto view = inner.As(); @@ -786,9 +792,15 @@ size_t JsBufferSource::size() const { bool JsBufferSource::isIntegerType() const { v8::Local inner = *this; - return inner->IsUint8Array() || inner->IsUint8ClampedArray() || inner->IsInt8Array() || - inner->IsUint16Array() || inner->IsInt16Array() || inner->IsUint32Array() || - inner->IsInt32Array() || inner->IsBigInt64Array() || inner->IsBigUint64Array(); + return inner->IsArrayBuffer() || inner->IsSharedArrayBuffer() || inner->IsUint8Array() || + inner->IsUint8ClampedArray() || inner->IsInt8Array() || inner->IsUint16Array() || + inner->IsInt16Array() || inner->IsUint32Array() || inner->IsInt32Array() || + inner->IsBigInt64Array() || inner->IsBigUint64Array(); +} + +bool JsBufferSource::isSharedArrayBuffer() const { + v8::Local inner = *this; + return inner->IsSharedArrayBuffer(); } bool JsBufferSource::isArrayBuffer() const { @@ -806,6 +818,16 @@ kj::Array JsBufferSource::copy() { return kj::heapArray(ptr); } +bool JsBufferSource::isResizable() const { + v8::Local inner = *this; + if (inner->IsArrayBuffer()) { + return inner.As()->IsResizableByUserJavaScript(); + } else if (inner->IsArrayBufferView()) { + return inner.As()->Buffer()->IsResizableByUserJavaScript(); + } + return false; +} + // ====================================================================================== // JsUint8Array diff --git a/src/workerd/jsg/jsvalue.h b/src/workerd/jsg/jsvalue.h index 7c7039c0d64..bee149d27e5 100644 --- a/src/workerd/jsg/jsvalue.h +++ b/src/workerd/jsg/jsvalue.h @@ -327,6 +327,8 @@ class JsBufferSource final: public JsBase { JsBufferSource(JsArrayBuffer& buffer): JsBase(static_cast>(buffer)) {} JsBufferSource(JsUint8Array& buffer): JsBase(static_cast>(buffer)) {} JsBufferSource(JsArrayBufferView& buffer): JsBase(static_cast>(buffer)) {} + JsBufferSource(v8::Local buffer) + : JsBase(static_cast>(buffer)) {} kj::ArrayPtr asArrayPtr(); @@ -335,8 +337,10 @@ class JsBufferSource final: public JsBase { // Returns true if the underlying value is an integer-typed TypedArray. bool isIntegerType() const; + bool isSharedArrayBuffer() const; bool isArrayBuffer() const; bool isArrayBufferView() const; + bool isResizable() const; // Return a copy of this buffer's data as a kj::Array. kj::Array copy();