LibWeb: Implement TransformStream transfer

This commit is contained in:
Timothy Flynn 2025-05-20 18:05:52 -04:00 committed by Tim Flynn
commit 95fc0a8070
Notes: github-actions[bot] 2025-05-21 10:55:48 +00:00
6 changed files with 231 additions and 18 deletions

View file

@ -50,6 +50,7 @@
#include <LibWeb/HTML/MessagePort.h> #include <LibWeb/HTML/MessagePort.h>
#include <LibWeb/HTML/StructuredSerialize.h> #include <LibWeb/HTML/StructuredSerialize.h>
#include <LibWeb/Streams/ReadableStream.h> #include <LibWeb/Streams/ReadableStream.h>
#include <LibWeb/Streams/TransformStream.h>
#include <LibWeb/Streams/WritableStream.h> #include <LibWeb/Streams/WritableStream.h>
#include <LibWeb/WebIDL/DOMException.h> #include <LibWeb/WebIDL/DOMException.h>
#include <LibWeb/WebIDL/ExceptionOr.h> #include <LibWeb/WebIDL/ExceptionOr.h>
@ -1299,6 +1300,8 @@ static bool is_interface_exposed_on_target_realm(TransferType name, JS::Realm& r
return intrinsics.is_exposed("ReadableStream"sv); return intrinsics.is_exposed("ReadableStream"sv);
case TransferType::WritableStream: case TransferType::WritableStream:
return intrinsics.is_exposed("WritableStream"sv); return intrinsics.is_exposed("WritableStream"sv);
case TransferType::TransformStream:
return intrinsics.is_exposed("TransformStream"sv);
case TransferType::Unknown: case TransferType::Unknown:
dbgln("Unknown interface type for transfer: {}", to_underlying(name)); dbgln("Unknown interface type for transfer: {}", to_underlying(name));
break; break;
@ -1326,6 +1329,11 @@ static WebIDL::ExceptionOr<GC::Ref<Bindings::PlatformObject>> create_transferred
TRY(writable_stream->transfer_receiving_steps(transfer_data_holder)); TRY(writable_stream->transfer_receiving_steps(transfer_data_holder));
return writable_stream; return writable_stream;
} }
case TransferType::TransformStream: {
auto transform_stream = target_realm.create<Streams::TransformStream>(target_realm);
TRY(transform_stream->transfer_receiving_steps(transfer_data_holder));
return transform_stream;
}
case TransferType::ArrayBuffer: case TransferType::ArrayBuffer:
case TransferType::ResizableArrayBuffer: case TransferType::ResizableArrayBuffer:
dbgln("ArrayBuffer ({}) is not a platform object.", to_underlying(name)); dbgln("ArrayBuffer ({}) is not a platform object.", to_underlying(name));

View file

@ -51,6 +51,7 @@ enum class TransferType : u8 {
ResizableArrayBuffer = 3, ResizableArrayBuffer = 3,
ReadableStream = 4, ReadableStream = 4,
WritableStream = 5, WritableStream = 5,
TransformStream = 6,
}; };
WebIDL::ExceptionOr<SerializationRecord> structured_serialize(JS::VM& vm, JS::Value); WebIDL::ExceptionOr<SerializationRecord> structured_serialize(JS::VM& vm, JS::Value);

View file

@ -4,15 +4,18 @@
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
#include <LibIPC/File.h>
#include <LibWeb/Bindings/Intrinsics.h> #include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/TransformStreamPrototype.h> #include <LibWeb/Bindings/TransformStreamPrototype.h>
#include <LibWeb/Streams/AbstractOperations.h> #include <LibWeb/Streams/AbstractOperations.h>
#include <LibWeb/Streams/ReadableStream.h> #include <LibWeb/Streams/ReadableStream.h>
#include <LibWeb/Streams/ReadableStreamOperations.h>
#include <LibWeb/Streams/TransformStream.h> #include <LibWeb/Streams/TransformStream.h>
#include <LibWeb/Streams/TransformStreamDefaultController.h> #include <LibWeb/Streams/TransformStreamDefaultController.h>
#include <LibWeb/Streams/TransformStreamOperations.h> #include <LibWeb/Streams/TransformStreamOperations.h>
#include <LibWeb/Streams/Transformer.h> #include <LibWeb/Streams/Transformer.h>
#include <LibWeb/Streams/WritableStream.h> #include <LibWeb/Streams/WritableStream.h>
#include <LibWeb/Streams/WritableStreamOperations.h>
#include <LibWeb/WebIDL/AbstractOperations.h> #include <LibWeb/WebIDL/AbstractOperations.h>
#include <LibWeb/WebIDL/ExceptionOr.h> #include <LibWeb/WebIDL/ExceptionOr.h>
@ -76,6 +79,28 @@ WebIDL::ExceptionOr<GC::Ref<TransformStream>> TransformStream::construct_impl(JS
return stream; return stream;
} }
TransformStream::TransformStream(JS::Realm& realm)
: Bindings::PlatformObject(realm)
{
}
TransformStream::~TransformStream() = default;
void TransformStream::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(TransformStream);
Base::initialize(realm);
}
void TransformStream::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_backpressure_change_promise);
visitor.visit(m_controller);
visitor.visit(m_readable);
visitor.visit(m_writable);
}
// https://streams.spec.whatwg.org/#transformstream-enqueue // https://streams.spec.whatwg.org/#transformstream-enqueue
void TransformStream::enqueue(JS::Value chunk) void TransformStream::enqueue(JS::Value chunk)
{ {
@ -161,26 +186,83 @@ void TransformStream::set_up(GC::Ref<TransformAlgorithm> transform_algorithm, GC
set_up_transform_stream_default_controller(*this, controller, transform_algorithm_wrapper, flush_algorithm_wrapper, cancel_algorithm_wrapper); set_up_transform_stream_default_controller(*this, controller, transform_algorithm_wrapper, flush_algorithm_wrapper, cancel_algorithm_wrapper);
} }
TransformStream::TransformStream(JS::Realm& realm) // https://streams.spec.whatwg.org/#ref-for-transfer-steps②
: Bindings::PlatformObject(realm) WebIDL::ExceptionOr<void> TransformStream::transfer_steps(HTML::TransferDataHolder& data_holder)
{ {
auto& realm = this->realm();
auto& vm = realm.vm();
auto serialize_stream = [&](auto stream) {
auto result = MUST(HTML::structured_serialize_with_transfer(vm, stream, { { GC::Root { stream } } }));
data_holder.data.extend(move(result.transfer_data_holders.first().data));
data_holder.fds.extend(move(result.transfer_data_holders.first().fds));
};
// 1. Let readable be value.[[readable]].
auto readable = this->readable();
// 2. Let writable be value.[[writable]].
auto writable = this->writable();
// 3. If ! IsReadableStreamLocked(readable) is true, throw a "DataCloneError" DOMException.
if (is_readable_stream_locked(readable))
return WebIDL::DataCloneError::create(realm, "Cannot transfer locked ReadableStream"_string);
// 4. If ! IsWritableStreamLocked(writable) is true, throw a "DataCloneError" DOMException.
if (is_writable_stream_locked(writable))
return WebIDL::DataCloneError::create(realm, "Cannot transfer locked WritableStream"_string);
// 5. Set dataHolder.[[readable]] to ! StructuredSerializeWithTransfer(readable, « readable »).
serialize_stream(readable);
// 6. Set dataHolder.[[writable]] to ! StructuredSerializeWithTransfer(writable, « writable »).
serialize_stream(writable);
return {};
} }
TransformStream::~TransformStream() = default; template<typename StreamType>
static WebIDL::ExceptionOr<GC::Ref<StreamType>> deserialize_stream(JS::Realm& realm, HTML::TransferDataHolder& data_holder)
void TransformStream::initialize(JS::Realm& realm)
{ {
WEB_SET_PROTOTYPE_FOR_INTERFACE(TransformStream); auto transfer_type = data_holder.data.take_first();
Base::initialize(realm);
if constexpr (IsSame<StreamType, ReadableStream>)
VERIFY(transfer_type == to_underlying(HTML::TransferType::ReadableStream));
else if constexpr (IsSame<StreamType, WritableStream>)
VERIFY(transfer_type == to_underlying(HTML::TransferType::WritableStream));
else
static_assert(DependentFalse<StreamType>);
auto stream = realm.create<StreamType>(realm);
TRY(stream->transfer_receiving_steps(data_holder));
return stream;
} }
void TransformStream::visit_edges(Cell::Visitor& visitor) // https://streams.spec.whatwg.org/#ref-for-transfer-receiving-steps②
WebIDL::ExceptionOr<void> TransformStream::transfer_receiving_steps(HTML::TransferDataHolder& data_holder)
{ {
Base::visit_edges(visitor); auto& realm = this->realm();
visitor.visit(m_backpressure_change_promise);
visitor.visit(m_controller); // 1. Let readableRecord be ! StructuredDeserializeWithTransfer(dataHolder.[[readable]], the current Realm).
visitor.visit(m_readable); auto readable = TRY(deserialize_stream<ReadableStream>(realm, data_holder));
visitor.visit(m_writable);
// 2. Let writableRecord be ! StructuredDeserializeWithTransfer(dataHolder.[[writable]], the current Realm).
auto writable = TRY(deserialize_stream<WritableStream>(realm, data_holder));
// 3. Set value.[[readable]] to readableRecord.[[Deserialized]].
set_readable(readable);
// 4. Set value.[[writable]] to writableRecord.[[Deserialized]].
set_writable(writable);
// 5. Set value.[[backpressure]], value.[[backpressureChangePromise]], and value.[[controller]] to undefined.
set_backpressure({});
set_backpressure_change_promise({});
set_controller({});
return {};
} }
} }

View file

@ -8,6 +8,7 @@
#include <LibJS/Forward.h> #include <LibJS/Forward.h>
#include <LibWeb/Bindings/PlatformObject.h> #include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/Bindings/Transferable.h>
#include <LibWeb/Forward.h> #include <LibWeb/Forward.h>
#include <LibWeb/Streams/Algorithms.h> #include <LibWeb/Streams/Algorithms.h>
#include <LibWeb/Streams/QueuingStrategy.h> #include <LibWeb/Streams/QueuingStrategy.h>
@ -15,7 +16,9 @@
namespace Web::Streams { namespace Web::Streams {
class TransformStream final : public Bindings::PlatformObject { class TransformStream final
: public Bindings::PlatformObject
, public Bindings::Transferable {
WEB_PLATFORM_OBJECT(TransformStream, Bindings::PlatformObject); WEB_PLATFORM_OBJECT(TransformStream, Bindings::PlatformObject);
GC_DECLARE_ALLOCATOR(TransformStream); GC_DECLARE_ALLOCATOR(TransformStream);
@ -44,6 +47,11 @@ public:
void set_up(GC::Ref<TransformAlgorithm>, GC::Ptr<FlushAlgorithm> = {}, GC::Ptr<CancelAlgorithm> = {}); void set_up(GC::Ref<TransformAlgorithm>, GC::Ptr<FlushAlgorithm> = {}, GC::Ptr<CancelAlgorithm> = {});
void enqueue(JS::Value chunk); void enqueue(JS::Value chunk);
// ^Transferable
virtual WebIDL::ExceptionOr<void> transfer_steps(HTML::TransferDataHolder&) override;
virtual WebIDL::ExceptionOr<void> transfer_receiving_steps(HTML::TransferDataHolder&) override;
virtual HTML::TransferType primary_interface() const override { return HTML::TransferType::TransformStream; }
private: private:
explicit TransformStream(JS::Realm& realm); explicit TransformStream(JS::Realm& realm);
@ -63,10 +71,6 @@ private:
// A TransformStreamDefaultController created with the ability to control [[readable]] and [[writable]] // A TransformStreamDefaultController created with the ability to control [[readable]] and [[writable]]
GC::Ptr<TransformStreamDefaultController> m_controller; GC::Ptr<TransformStreamDefaultController> m_controller;
// https://streams.spec.whatwg.org/#transformstream-detached
// A boolean flag set to true when the stream is transferred
bool m_detached { false };
// https://streams.spec.whatwg.org/#transformstream-readable // https://streams.spec.whatwg.org/#transformstream-readable
// The ReadableStream instance controlled by this object // The ReadableStream instance controlled by this object
GC::Ptr<ReadableStream> m_readable; GC::Ptr<ReadableStream> m_readable;

View file

@ -0,0 +1,10 @@
Harness status: OK
Found 5 tests
5 Pass
Pass window.postMessage should be able to transfer a TransformStream
Pass a TransformStream with a locked writable should not be transferable
Pass a TransformStream with a locked readable should not be transferable
Pass a TransformStream with both sides locked should not be transferable
Pass piping through transferred transforms should work

View file

@ -0,0 +1,108 @@
<!DOCTYPE html>
<meta charset="utf-8">
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="../resources/test-utils.js"></script>
<script>
'use strict';
promise_test(t => {
const orig = new TransformStream();
const promise = new Promise(resolve => {
addEventListener('message', t.step_func(evt => {
const transferred = evt.data;
assert_equals(transferred.constructor, TransformStream,
'transferred should be a TransformStream in this realm');
assert_true(transferred instanceof TransformStream,
'instanceof check should pass');
// Perform a brand-check on |transferred|.
const readableGetter = Object.getOwnPropertyDescriptor(
TransformStream.prototype, 'readable').get;
assert_true(readableGetter.call(transferred) instanceof ReadableStream,
'brand check should pass and readable stream should result');
const writableGetter = Object.getOwnPropertyDescriptor(
TransformStream.prototype, 'writable').get;
assert_true(writableGetter.call(transferred) instanceof WritableStream,
'brand check should pass and writable stream should result');
resolve();
}), {once: true});
});
postMessage(orig, '*', [orig]);
assert_true(orig.readable.locked, 'the readable side should be locked');
assert_true(orig.writable.locked, 'the writable side should be locked');
return promise;
}, 'window.postMessage should be able to transfer a TransformStream');
test(() => {
const ts = new TransformStream();
const writer = ts.writable.getWriter();
assert_throws_dom('DataCloneError', () => postMessage(ts, '*', [ts]),
'postMessage should throw');
assert_false(ts.readable.locked, 'readable side should not get locked');
}, 'a TransformStream with a locked writable should not be transferable');
test(() => {
const ts = new TransformStream();
const reader = ts.readable.getReader();
assert_throws_dom('DataCloneError', () => postMessage(ts, '*', [ts]),
'postMessage should throw');
assert_false(ts.writable.locked, 'writable side should not get locked');
}, 'a TransformStream with a locked readable should not be transferable');
test(() => {
const ts = new TransformStream();
const reader = ts.readable.getReader();
const writer = ts.writable.getWriter();
assert_throws_dom('DataCloneError', () => postMessage(ts, '*', [ts]),
'postMessage should throw');
}, 'a TransformStream with both sides locked should not be transferable');
promise_test(t => {
const source = new ReadableStream({
start(controller) {
controller.enqueue('hello ');
controller.enqueue('there ');
controller.close();
}
});
let resolve;
const ready = new Promise(r => resolve = r);
let result = '';
const sink = new WritableStream({
write(chunk) {
if (result) {
resolve();
}
result += chunk;
}
});
const transform1 = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase());
}
});
const transform2 = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk + chunk);
}
});
const promise = new Promise(resolve => {
addEventListener('message', t.step_func(evt => {
const data = evt.data;
resolve(data.source
.pipeThrough(data.transform1)
.pipeThrough(data.transform2)
.pipeTo(data.sink));
}));
});
postMessage({source, sink, transform1, transform2}, '*',
[source, transform1, sink, transform2]);
return ready
.then(() => {
assert_equals(result, 'HELLO HELLO THERE THERE ',
'transforms should have been applied');
});
}, 'piping through transferred transforms should work');
</script>