/* * Copyright (c) 2022, Linus Groh * Copyright (c) 2023, Matthew Olsson * Copyright (c) 2023-2025, Shannon Booth * Copyright (c) 2023-2024, Kenneth Myhra * Copyright (c) 2025, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Web::Streams { // https://streams.spec.whatwg.org/#validate-and-normalize-high-water-mark WebIDL::ExceptionOr extract_high_water_mark(QueuingStrategy const& strategy, double default_hwm) { // 1. If strategy["highWaterMark"] does not exist, return defaultHWM. if (!strategy.high_water_mark.has_value()) return default_hwm; // 2. Let highWaterMark be strategy["highWaterMark"]. auto high_water_mark = strategy.high_water_mark.value(); // 3. If highWaterMark is NaN or highWaterMark < 0, throw a RangeError exception. if (isnan(high_water_mark) || high_water_mark < 0) return WebIDL::SimpleException { WebIDL::SimpleExceptionType::RangeError, "Invalid value for high water mark"sv }; // 4. Return highWaterMark. return high_water_mark; } // https://streams.spec.whatwg.org/#make-size-algorithm-from-size-function GC::Ref extract_size_algorithm(JS::VM& vm, QueuingStrategy const& strategy) { // 1. If strategy["size"] does not exist, return an algorithm that returns 1. if (!strategy.size) return GC::create_function(vm.heap(), [](JS::Value) { return JS::normal_completion(JS::Value(1)); }); // 2. Return an algorithm that performs the following steps, taking a chunk argument: return GC::create_function(vm.heap(), [size = strategy.size](JS::Value chunk) { // 1. Return the result of invoking strategy["size"] with argument list « chunk ». return WebIDL::invoke_callback(*size, {}, { { chunk } }); }); } struct PromiseHolder : public JS::Cell { GC_CELL(PromiseHolder, JS::Cell); GC_DECLARE_ALLOCATOR(PromiseHolder); explicit PromiseHolder(GC::Ptr promise) : promise(promise) { } virtual void visit_edges(JS::Cell::Visitor& visitor) override { Base::visit_edges(visitor); visitor.visit(promise); } GC::Ptr promise; }; GC_DEFINE_ALLOCATOR(PromiseHolder); static void add_message_event_listener(JS::Realm& realm, HTML::MessagePort& port, FlyString const& name, Function handler) { auto behavior = [handler = GC::create_function(realm.heap(), move(handler))](JS::VM& vm) { auto event = vm.argument(0); VERIFY(event.is_object()); auto& message_event = as(event.as_object()); handler->function()(vm, message_event); return JS::js_undefined(); }; auto function = JS::NativeFunction::create(realm, move(behavior), 1, FlyString {}, &realm); auto callback = realm.heap().allocate(function, realm); auto listener = DOM::IDLEventListener::create(realm, callback); port.add_event_listener_without_options(name, listener); } // https://streams.spec.whatwg.org/#abstract-opdef-crossrealmtransformsenderror void cross_realm_transform_send_error(JS::Realm& realm, HTML::MessagePort& port, JS::Value error) { // 1. Perform PackAndPostMessage(port, "error", error), discarding the result. (void)pack_and_post_message(realm, port, "error"sv, error); } // https://streams.spec.whatwg.org/#abstract-opdef-packandpostmessagehandlingerror WebIDL::ExceptionOr pack_and_post_message(JS::Realm& realm, HTML::MessagePort& port, StringView type, JS::Value value) { auto& vm = realm.vm(); // 1. Let message be OrdinaryObjectCreate(null). auto message = JS::Object::create(realm, nullptr); // 2. Perform ! CreateDataProperty(message, "type", type). MUST(message->create_data_property(vm.names.type, JS::PrimitiveString::create(vm, type))); // 3. Perform ! CreateDataProperty(message, "value", value). MUST(message->create_data_property(vm.names.value, value)); // 4. Let targetPort be the port with which port is entangled, if any; otherwise let it be null. auto target_port = port.entangled_port(); // 5. Let options be «[ "transfer" → « » ]». HTML::StructuredSerializeOptions options { .transfer = {} }; // 6. Run the message port post message steps providing targetPort, message, and options. return port.message_port_post_message_steps(target_port, message, options); } // https://streams.spec.whatwg.org/#abstract-opdef-packandpostmessagehandlingerror WebIDL::ExceptionOr pack_and_post_message_handling_error(JS::Realm& realm, HTML::MessagePort& port, StringView type, JS::Value value) { // 1. Let result be PackAndPostMessage(port, type, value). auto result = pack_and_post_message(realm, port, type, value); // 2. If result is an abrupt completion, if (result.is_exception()) { auto error = Bindings::exception_to_throw_completion(realm.vm(), result.release_error()); // 1. Perform ! CrossRealmTransformSendError(port, result.[[Value]]). cross_realm_transform_send_error(realm, port, error.value()); } // 3. Return result as a completion record. return result; } // https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable void set_up_cross_realm_transform_readable(JS::Realm& realm, ReadableStream& stream, HTML::MessagePort& port) { // 1. Perform ! InitializeReadableStream(stream). initialize_readable_stream(stream); // 2. Let controller be a new ReadableStreamDefaultController. auto controller = realm.create(realm); // 3. Add a handler for port’s message event with the following steps: add_message_event_listener(realm, port, HTML::EventNames::message, [&port, controller](JS::VM& vm, HTML::MessageEvent const& message) { // 1. Let data be the data of the message. auto data = message.data(); // 2. Assert: data is an Object. VERIFY(data.is_object()); // 3. Let type be ! Get(data, "type"). auto type = MUST(data.get(vm, vm.names.type)); // 4. Let value be ! Get(data, "value"). auto value = MUST(data.get(vm, vm.names.value)); // 5. Assert: type is a String. auto type_string = type.as_string().utf8_string_view(); // 6. If type is "chunk", if (type_string == "chunk"sv) { // 1. Perform ! ReadableStreamDefaultControllerEnqueue(controller, value). MUST(readable_stream_default_controller_enqueue(controller, value)); } // 7. Otherwise, if type is "close", else if (type_string == "close"sv) { // 1. Perform ! ReadableStreamDefaultControllerClose(controller). readable_stream_default_controller_close(controller); // 2. Disentangle port. port.disentangle(); } // 8. Otherwise, if type is "error", else if (type_string == "error"sv) { // 1. Perform ! ReadableStreamDefaultControllerError(controller, value). readable_stream_default_controller_error(controller, value); // 2. Disentangle port. port.disentangle(); } }); // 4. Add a handler for port’s messageerror event with the following steps: add_message_event_listener(realm, port, HTML::EventNames::messageerror, [&realm, &port, controller](JS::VM&, HTML::MessageEvent const&) { // 1. Let error be a new "DataCloneError" DOMException. auto error = WebIDL::DataCloneError::create(realm, "Unable to transfer stream"_string); // 2. Perform ! CrossRealmTransformSendError(port, error). cross_realm_transform_send_error(realm, port, error); // 3. Perform ! ReadableStreamDefaultControllerError(controller, error). readable_stream_default_controller_error(controller, error); // 4. Disentangle port. port.disentangle(); }); // FIXME: 5. Enable port’s port message queue. // 6. Let startAlgorithm be an algorithm that returns undefined. auto start_algorithm = GC::create_function(realm.heap(), []() -> WebIDL::ExceptionOr { return JS::js_undefined(); }); // 7. Let pullAlgorithm be the following steps: auto pull_algorithm = GC::create_function(realm.heap(), [&realm, &port]() -> GC::Ref { // 1. Perform ! PackAndPostMessage(port, "pull", undefined). MUST(pack_and_post_message(realm, port, "pull"sv, JS::js_undefined())); // 2. Return a promise resolved with undefined. return WebIDL::create_resolved_promise(realm, JS::js_undefined()); }); // 8. Let cancelAlgorithm be the following steps, taking a reason argument: auto cancel_algorithm = GC::create_function(realm.heap(), [&realm, &port](JS::Value reason) -> GC::Ref { // 1. Let result be PackAndPostMessageHandlingError(port, "error", reason). auto result = pack_and_post_message_handling_error(realm, port, "error"sv, reason); // 2. Disentangle port. port.disentangle(); // 3. If result is an abrupt completion, return a promise rejected with result.[[Value]]. if (result.is_error()) return WebIDL::create_rejected_promise_from_exception(realm, result.release_error()); // 4. Otherwise, return a promise resolved with undefined. return WebIDL::create_resolved_promise(realm, JS::js_undefined()); }); // 9. Let sizeAlgorithm be an algorithm that returns 1. auto size_algorithm = GC::create_function(realm.heap(), [](JS::Value) -> JS::Completion { return JS::Value { 1 }; }); // 10. Perform ! SetUpReadableStreamDefaultController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, 0, sizeAlgorithm). MUST(set_up_readable_stream_default_controller(stream, controller, start_algorithm, pull_algorithm, cancel_algorithm, 0, size_algorithm)); } // https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable void set_up_cross_realm_transform_writable(JS::Realm& realm, WritableStream& stream, HTML::MessagePort& port) { // 1. Perform ! InitializeWritableStream(stream). initialize_writable_stream(stream); // 2. Let controller be a new WritableStreamDefaultController. auto controller = realm.create(realm); // 3. Let backpressurePromise be a new promise. auto backpressure_promise = realm.heap().allocate(WebIDL::create_promise(realm)); // 4. Add a handler for port’s message event with the following steps: add_message_event_listener(realm, port, HTML::EventNames::message, [&realm, controller, backpressure_promise](JS::VM& vm, HTML::MessageEvent const& message) { // 1. Let data be the data of the message. auto data = message.data(); // 2. Assert: data is an Object. VERIFY(data.is_object()); // 3. Let type be ! Get(data, "type"). auto type = MUST(data.get(vm, vm.names.type)); // 4. Let value be ! Get(data, "value"). auto value = MUST(data.get(vm, vm.names.value)); // 5. Assert: type is a String. auto type_string = type.as_string().utf8_string_view(); // 6. If type is "pull", if (type_string == "pull"sv) { // 1. If backpressurePromise is not undefined, if (backpressure_promise->promise) { // 1. Resolve backpressurePromise with undefined. WebIDL::resolve_promise(realm, *backpressure_promise->promise, JS::js_undefined()); // 2. Set backpressurePromise to undefined. backpressure_promise->promise = nullptr; } } // 7. Otherwise, if type is "error", else if (type_string == "error"sv) { // 1. Perform ! WritableStreamDefaultControllerErrorIfNeeded(controller, value). writable_stream_default_controller_error_if_needed(controller, value); // 2. If backpressurePromise is not undefined, if (backpressure_promise->promise) { // 1. Resolve backpressurePromise with undefined. WebIDL::resolve_promise(realm, *backpressure_promise->promise, JS::js_undefined()); // 2. Set backpressurePromise to undefined. backpressure_promise->promise = nullptr; } } }); // 5. Add a handler for port’s messageerror event with the following steps: add_message_event_listener(realm, port, HTML::EventNames::messageerror, [&realm, &port, controller](JS::VM&, HTML::MessageEvent const&) { // 1. Let error be a new "DataCloneError" DOMException auto error = WebIDL::DataCloneError::create(realm, "Unable to transfer stream"_string); // 2. Perform ! CrossRealmTransformSendError(port, error). cross_realm_transform_send_error(realm, port, error); // 3. Perform ! WritableStreamDefaultControllerErrorIfNeeded(controller, error). writable_stream_default_controller_error_if_needed(controller, error); // 4. Disentangle port. port.disentangle(); }); // FIXME: 6. Enable port’s port message queue. // 7. Let startAlgorithm be an algorithm that returns undefined. auto start_algorithm = GC::create_function(realm.heap(), []() -> WebIDL::ExceptionOr { return JS::js_undefined(); }); // 8. Let writeAlgorithm be the following steps, taking a chunk argument: auto write_algorithm = GC::create_function(realm.heap(), [&realm, &port, backpressure_promise](JS::Value chunk) -> GC::Ref { // 1. If backpressurePromise is undefined, set backpressurePromise to a promise resolved with undefined. if (!backpressure_promise->promise) backpressure_promise->promise = WebIDL::create_resolved_promise(realm, JS::js_undefined()); // FIXME: The steps below ("Return a promise rejected/resolved with ...") seem to indicate we should be creating // a promise on-the-fly. But in order for the error from PackAndPostMessageHandlingError to be propagated // back to the original ReadableStream, we must actually fulfill the promise created from reacting to the // backpressure promise. This is explicitly tested by WPT. auto reaction_promise = realm.heap().allocate(nullptr); // 2. Return the result of reacting to backpressurePromise with the following fulfillment steps: reaction_promise->promise = WebIDL::upon_fulfillment(*backpressure_promise->promise, GC::create_function(realm.heap(), [&realm, &port, backpressure_promise, reaction_promise, chunk](JS::Value) -> WebIDL::ExceptionOr { // 1. Set backpressurePromise to a new promise. backpressure_promise->promise = WebIDL::create_promise(realm); // 2. Let result be PackAndPostMessageHandlingError(port, "chunk", chunk). auto result = pack_and_post_message_handling_error(realm, port, "chunk"sv, chunk); // 3. If result is an abrupt completion, if (result.is_error()) { // 1. Disentangle port. port.disentangle(); // 2. Return a promise rejected with result.[[Value]]. auto error = Bindings::exception_to_throw_completion(realm.vm(), result.release_error()); WebIDL::reject_promise(realm, *reaction_promise->promise, error.value()); } // 4. Otherwise, return a promise resolved with undefined. else { WebIDL::resolve_promise(realm, *reaction_promise->promise, JS::js_undefined()); } return reaction_promise->promise; })); return *reaction_promise->promise; }); // 9. Let closeAlgorithm be the folowing steps: auto close_algorithm = GC::create_function(realm.heap(), [&realm, &port]() -> GC::Ref { // 1. Perform ! PackAndPostMessage(port, "close", undefined). MUST(pack_and_post_message(realm, port, "close"sv, JS::js_undefined())); // 2. Disentangle port. port.disentangle(); // 3. Return a promise resolved with undefined. return WebIDL::create_resolved_promise(realm, JS::js_undefined()); }); // 10. Let abortAlgorithm be the following steps, taking a reason argument: auto abort_algorithm = GC::create_function(realm.heap(), [&realm, &port](JS::Value reason) -> GC::Ref { // 1. Let result be PackAndPostMessageHandlingError(port, "error", reason). auto result = pack_and_post_message_handling_error(realm, port, "error"sv, reason); // 2. Disentangle port. port.disentangle(); // 3. If result is an abrupt completion, return a promise rejected with result.[[Value]]. if (result.is_error()) return WebIDL::create_rejected_promise_from_exception(realm, result.release_error()); // 4. Otherwise, return a promise resolved with undefined. return WebIDL::create_resolved_promise(realm, JS::js_undefined()); }); // 11. Let sizeAlgorithm be an algorithm that returns 1. auto size_algorithm = GC::create_function(realm.heap(), [](JS::Value) -> JS::Completion { return JS::Value { 1 }; }); // 12. Perform ! SetUpWritableStreamDefaultController(stream, controller, startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, 1, sizeAlgorithm). MUST(set_up_writable_stream_default_controller(stream, controller, start_algorithm, write_algorithm, close_algorithm, abort_algorithm, 1, size_algorithm)); } // https://streams.spec.whatwg.org/#can-transfer-array-buffer bool can_transfer_array_buffer(JS::ArrayBuffer const& array_buffer) { // 1. Assert: O is an Object. // 2. Assert: O has an [[ArrayBufferData]] internal slot. // 3. If ! IsDetachedBuffer(O) is true, return false. if (array_buffer.is_detached()) return false; // 4. If SameValue(O.[[ArrayBufferDetachKey]], undefined) is false, return false. if (!JS::same_value(array_buffer.detach_key(), JS::js_undefined())) return false; // 5. Return true. return true; } // https://streams.spec.whatwg.org/#is-non-negative-number bool is_non_negative_number(JS::Value value) { // 1. If v is not a Number, return false. if (!value.is_number()) return false; // 2. If v is NaN, return false. if (value.is_nan()) return false; // 3. If v < 0, return false. if (value.as_double() < 0.0) return false; // 4. Return true. return true; } // https://streams.spec.whatwg.org/#transfer-array-buffer WebIDL::ExceptionOr> transfer_array_buffer(JS::Realm& realm, JS::ArrayBuffer& buffer) { auto& vm = realm.vm(); // 1. Assert: ! IsDetachedBuffer(O) is false. VERIFY(!buffer.is_detached()); // 2. Let arrayBufferData be O.[[ArrayBufferData]]. // 3. Let arrayBufferByteLength be O.[[ArrayBufferByteLength]]. auto array_buffer = buffer.buffer(); // 4. Perform ? DetachArrayBuffer(O). TRY(JS::detach_array_buffer(vm, buffer)); // 5. Return a new ArrayBuffer object, created in the current Realm, whose [[ArrayBufferData]] internal slot value is arrayBufferData and whose [[ArrayBufferByteLength]] internal slot value is arrayBufferByteLength. return JS::ArrayBuffer::create(realm, move(array_buffer)); } // https://streams.spec.whatwg.org/#abstract-opdef-cloneasuint8array WebIDL::ExceptionOr clone_as_uint8_array(JS::Realm& realm, WebIDL::ArrayBufferView& view) { auto& vm = realm.vm(); // 1. Assert: O is an Object. // 2. Assert: O has an [[ViewedArrayBuffer]] internal slot. // 3. Assert: ! IsDetachedBuffer(O.[[ViewedArrayBuffer]]) is false. VERIFY(!view.viewed_array_buffer()->is_detached()); // 4. Let buffer be ? CloneArrayBuffer(O.[[ViewedArrayBuffer]], O.[[ByteOffset]], O.[[ByteLength]], %ArrayBuffer%). auto* buffer = TRY(JS::clone_array_buffer(vm, *view.viewed_array_buffer(), view.byte_offset(), view.byte_length())); // 5. Let array be ! Construct(%Uint8Array%, « buffer »). auto array = MUST(JS::construct(vm, *realm.intrinsics().uint8_array_constructor(), buffer)); // 5. Return array. return array; } // https://streams.spec.whatwg.org/#abstract-opdef-structuredclone WebIDL::ExceptionOr structured_clone(JS::Realm& realm, JS::Value value) { auto& vm = realm.vm(); // 1. Let serialized be ? StructuredSerialize(v). auto serialized = TRY(HTML::structured_serialize(vm, value)); // 2. Return ? StructuredDeserialize(serialized, the current Realm). return TRY(HTML::structured_deserialize(vm, serialized, realm)); } // https://streams.spec.whatwg.org/#abstract-opdef-cancopydatablockbytes bool can_copy_data_block_bytes_buffer(JS::ArrayBuffer const& to_buffer, u64 to_index, JS::ArrayBuffer const& from_buffer, u64 from_index, u64 count) { // 1. Assert: toBuffer is an Object. // 2. Assert: toBuffer has an [[ArrayBufferData]] internal slot. // 3. Assert: fromBuffer is an Object. // 4. Assert: fromBuffer has an [[ArrayBufferData]] internal slot. // 5. If toBuffer is fromBuffer, return false. if (&to_buffer == &from_buffer) return false; // 6. If ! IsDetachedBuffer(toBuffer) is true, return false. if (to_buffer.is_detached()) return false; // 7. If ! IsDetachedBuffer(fromBuffer) is true, return false. if (from_buffer.is_detached()) return false; // 8. If toIndex + count > toBuffer.[[ArrayBufferByteLength]], return false. if (to_index + count > to_buffer.byte_length()) return false; // 9. If fromIndex + count > fromBuffer.[[ArrayBufferByteLength]], return false. if (from_index + count > from_buffer.byte_length()) return false; // 10. Return true. return true; } }