From 907dc84c1e8c3c236ade581f46dabdb144915c1d Mon Sep 17 00:00:00 2001 From: Kenneth Myhra Date: Mon, 8 Jul 2024 16:39:52 +0200 Subject: [PATCH] LibWeb: Implement min option for ReadableStreamBYOBReader.read() When the min option is given the read will only be fulfilled when there are min or more elements available in the readable byte stream. When the min option is not given the default value for min is 1. --- .../ReadableStreamBYOBReader-read-min.txt | 4 ++ .../ReadableStreamBYOBReader-read-min.html | 68 ++++++++++++++++++ .../LibWeb/Streams/AbstractOperations.cpp | 69 +++++++++++-------- .../LibWeb/Streams/AbstractOperations.h | 5 +- .../Streams/ReadableByteStreamController.cpp | 1 + .../Streams/ReadableByteStreamController.h | 4 ++ .../Streams/ReadableStreamBYOBReader.cpp | 40 +++++++++-- .../LibWeb/Streams/ReadableStreamBYOBReader.h | 8 ++- .../Streams/ReadableStreamBYOBReader.idl | 6 +- 9 files changed, 165 insertions(+), 40 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/Streams/ReadableStreamBYOBReader-read-min.txt create mode 100644 Tests/LibWeb/Text/input/Streams/ReadableStreamBYOBReader-read-min.html diff --git a/Tests/LibWeb/Text/expected/Streams/ReadableStreamBYOBReader-read-min.txt b/Tests/LibWeb/Text/expected/Streams/ReadableStreamBYOBReader-read-min.txt new file mode 100644 index 00000000000..ff260b38035 --- /dev/null +++ b/Tests/LibWeb/Text/expected/Streams/ReadableStreamBYOBReader-read-min.txt @@ -0,0 +1,4 @@ +This -⌄ + will not be processed as one chunk +This -> will be processed as one chunk +Stream closed diff --git a/Tests/LibWeb/Text/input/Streams/ReadableStreamBYOBReader-read-min.html b/Tests/LibWeb/Text/input/Streams/ReadableStreamBYOBReader-read-min.html new file mode 100644 index 00000000000..e446f8b3ec8 --- /dev/null +++ b/Tests/LibWeb/Text/input/Streams/ReadableStreamBYOBReader-read-min.html @@ -0,0 +1,68 @@ + + diff --git a/Userland/Libraries/LibWeb/Streams/AbstractOperations.cpp b/Userland/Libraries/LibWeb/Streams/AbstractOperations.cpp index 26fbe605e41..c1602bd90f3 100644 --- a/Userland/Libraries/LibWeb/Streams/AbstractOperations.cpp +++ b/Userland/Libraries/LibWeb/Streams/AbstractOperations.cpp @@ -1133,7 +1133,7 @@ WebIDL::ExceptionOr readable_byte_stream_tee(JS::Realm& real auto read_into_request = realm.heap().allocate_without_realm(realm, stream, params, cancel_promise, *byob_branch, *other_branch, for_branch2); // 5. Perform ! ReadableStreamBYOBReaderRead(reader, view, 1, readIntoRequest). - readable_stream_byob_reader_read(params->reader.get>(), view, read_into_request); + readable_stream_byob_reader_read(params->reader.get>(), view, 1, read_into_request); }); // 17. Let pull1Algorithm be the following steps: @@ -1656,34 +1656,36 @@ void readable_byte_stream_controller_fill_head_pull_into_descriptor(ReadableByte // https://streams.spec.whatwg.org/#readable-byte-stream-controller-fill-pull-into-descriptor-from-queue bool readable_byte_stream_controller_fill_pull_into_descriptor_from_queue(ReadableByteStreamController& controller, PullIntoDescriptor& pull_into_descriptor) { - // 1. Let elementSize be pullIntoDescriptor.[[elementSize]]. - auto element_size = pull_into_descriptor.element_size; - - // 2. Let currentAlignedBytes be pullIntoDescriptor’s bytes filled − (pullIntoDescriptor’s bytes filled mod elementSize). - auto current_aligned_bytes = pull_into_descriptor.bytes_filled - (pull_into_descriptor.bytes_filled % pull_into_descriptor.element_size); - - // 3. Let maxBytesToCopy be min(controller.[[queueTotalSize]], pullIntoDescriptor’s byte length − pullIntoDescriptor’s bytes filled). + // 1. Let maxBytesToCopy be min(controller.[[queueTotalSize]], pullIntoDescriptor’s byte length − pullIntoDescriptor’s bytes filled). auto max_bytes_to_copy = min(controller.queue_total_size(), pull_into_descriptor.byte_length - pull_into_descriptor.bytes_filled); - // 4. Let maxBytesFilled be pullIntoDescriptor’s bytes filled + maxBytesToCopy. + // 2. Let maxBytesFilled be pullIntoDescriptor’s bytes filled + maxBytesToCopy. u64 max_bytes_filled = pull_into_descriptor.bytes_filled + max_bytes_to_copy; - // 5. Let maxAlignedBytes be maxBytesFilled − (maxBytesFilled mod elementSize). - auto max_aligned_bytes = max_bytes_filled - (max_bytes_filled % element_size); - - // 6. Let totalBytesToCopyRemaining be maxBytesToCopy. + // 3. Let totalBytesToCopyRemaining be maxBytesToCopy. auto total_bytes_to_copy_remaining = max_bytes_to_copy; - // 7. Let ready be false. + // 4. Let ready be false. bool ready = false; - // 8. If maxAlignedBytes > currentAlignedBytes, - if (max_aligned_bytes > current_aligned_bytes) { + // 5. Assert: pullIntoDescriptor’s bytes filled < pullIntoDescriptor’s minimum fill. + VERIFY(pull_into_descriptor.bytes_filled < pull_into_descriptor.minimum_fill); + + // 6. Let remainderBytes be the remainder after dividing maxBytesFilled by pullIntoDescriptor’s element size. + auto remainder_bytes = max_bytes_filled % pull_into_descriptor.element_size; + + // 7. Let maxAlignedBytes be maxBytesFilled − remainderBytes. + auto max_aligned_bytes = max_bytes_filled - remainder_bytes; + + // 8. If maxAlignedBytes ≥ pullIntoDescriptor’s minimum fill, + if (max_aligned_bytes >= pull_into_descriptor.minimum_fill) { // 1. Set totalBytesToCopyRemaining to maxAlignedBytes − pullIntoDescriptor’s bytes filled. total_bytes_to_copy_remaining = max_aligned_bytes - pull_into_descriptor.bytes_filled; // 2. Set ready to true. ready = true; + + // NOTE: A descriptor for a read() request that is not yet filled up to its minimum length will stay at the head of the queue, so the underlying source can keep filling it. } // 9. Let queue be controller.[[queue]]. @@ -1735,8 +1737,8 @@ bool readable_byte_stream_controller_fill_pull_into_descriptor_from_queue(Readab // 2. Assert: pullIntoDescriptor’s bytes filled > 0. VERIFY(pull_into_descriptor.bytes_filled > 0); - // 3. Assert: pullIntoDescriptor’s bytes filled < pullIntoDescriptor’s element size. - VERIFY(pull_into_descriptor.bytes_filled < pull_into_descriptor.element_size); + // 3. Assert: pullIntoDescriptor’s bytes filled < pullIntoDescriptor’s minimum fill. + VERIFY(pull_into_descriptor.bytes_filled < pull_into_descriptor.minimum_fill); } // 12. Return ready. @@ -1789,7 +1791,7 @@ JS::Value readable_byte_stream_controller_convert_pull_into_descriptor(JS::Realm // 3. Assert: bytesFilled ≤ pullIntoDescriptor’s byte length. VERIFY(bytes_filled <= pull_into_descriptor.byte_length); - // 4. Assert: bytesFilled mod elementSize is 0. + // 4. Assert: the remainder after dividing bytesFilled by elementSize is 0. VERIFY(bytes_filled % element_size == 0); // 5. Let buffer be ! TransferArrayBuffer(pullIntoDescriptor’s buffer). @@ -1800,7 +1802,7 @@ JS::Value readable_byte_stream_controller_convert_pull_into_descriptor(JS::Realm } // https://streams.spec.whatwg.org/#readable-byte-stream-controller-pull-into -void readable_byte_stream_controller_pull_into(ReadableByteStreamController& controller, WebIDL::ArrayBufferView& view, ReadIntoRequest& read_into_request) +void readable_byte_stream_controller_pull_into(ReadableByteStreamController& controller, WebIDL::ArrayBufferView& view, u64 min, ReadIntoRequest& read_into_request) { auto& vm = controller.vm(); auto& realm = controller.realm(); @@ -1832,7 +1834,16 @@ void readable_byte_stream_controller_pull_into(ReadableByteStreamController& con } } - // 5. Let byteOffset be view.[[ByteOffset]]. + // 5. Let minimumFill be min × elementSize. + u64 minimum_fill = min * element_size; + + // 6. Assert: minimumFill ≥ 0 and minimumFill ≤ view.[[ByteLength]]. + VERIFY(minimum_fill <= view.byte_length()); + + // 7. Assert: the remainder after dividing minimumFill by elementSize is 0. + VERIFY(minimum_fill % element_size == 0); + + // 8. Let byteOffset be view.[[ByteOffset]]. auto byte_offset = view.byte_offset(); // 6. Let byteLength be view.[[ByteLength]]. @@ -1862,6 +1873,7 @@ void readable_byte_stream_controller_pull_into(ReadableByteStreamController& con .byte_offset = byte_offset, .byte_length = byte_length, .bytes_filled = 0, + .minimum_fill = minimum_fill, .element_size = element_size, .view_constructor = *ctor, .reader_type = ReaderType::Byob, @@ -1935,7 +1947,7 @@ void readable_byte_stream_controller_pull_into(ReadableByteStreamController& con } // https://streams.spec.whatwg.org/#readable-stream-byob-reader-read -void readable_stream_byob_reader_read(ReadableStreamBYOBReader& reader, WebIDL::ArrayBufferView& view, ReadIntoRequest& read_into_request) +void readable_stream_byob_reader_read(ReadableStreamBYOBReader& reader, WebIDL::ArrayBufferView& view, u64 min, ReadIntoRequest& read_into_request) { // 1. Let stream be reader.[[stream]]. auto stream = reader.stream(); @@ -1952,7 +1964,7 @@ void readable_stream_byob_reader_read(ReadableStreamBYOBReader& reader, WebIDL:: } // 5. Otherwise, perform ! ReadableByteStreamControllerPullInto(stream.[[controller]], view, readIntoRequest). else { - readable_byte_stream_controller_pull_into(*stream->controller()->get>(), view, read_into_request); + readable_byte_stream_controller_pull_into(*stream->controller()->get>(), view, min, read_into_request); } } @@ -2261,8 +2273,7 @@ WebIDL::ExceptionOr readable_byte_stream_controller_respond_in_readable_st } // 4. If pullIntoDescriptor’s bytes filled < pullIntoDescriptor’s minimum fill, return. - // FIXME: Support minimum fill. - if (pull_into_descriptor.bytes_filled < pull_into_descriptor.element_size) + if (pull_into_descriptor.bytes_filled < pull_into_descriptor.minimum_fill) return {}; // NOTE: A descriptor for a read() request that is not yet filled up to its minimum length will stay at the head of the queue, so the underlying source can keep filling it. @@ -2722,8 +2733,8 @@ WebIDL::ExceptionOr readable_byte_stream_controller_close(ReadableByteStre // 1. Let firstPendingPullInto be controller.[[pendingPullIntos]][0]. auto& first_pending_pull_into = controller.pending_pull_intos().first(); - // 2. If firstPendingPullInto’s bytes filled > 0, - if (first_pending_pull_into.bytes_filled > 0) { + // 2. If the remainder after dividing firstPendingPullInto’s bytes filled by firstPendingPullInto’s element size is not 0, + if (first_pending_pull_into.bytes_filled % first_pending_pull_into.element_size != 0) { // 1. Let e be a new TypeError exception. auto error = JS::TypeError::create(realm, "Cannot close controller in the middle of processing a write request"sv); @@ -3454,8 +3465,8 @@ void readable_byte_stream_controller_commit_pull_into_descriptor(ReadableStream& // 4. If stream.[[state]] is "closed", if (stream.is_closed()) { - // 1. Assert: pullIntoDescriptor’s bytes filled is 0. - VERIFY(pull_into_descriptor.bytes_filled == 0); + // 1. Assert: the remainder after dividing pullIntoDescriptor’s bytes filled by pullIntoDescriptor’s element size is 0. + VERIFY(pull_into_descriptor.bytes_filled % pull_into_descriptor.element_size == 0); // 2. Set done to true. done = true; diff --git a/Userland/Libraries/LibWeb/Streams/AbstractOperations.h b/Userland/Libraries/LibWeb/Streams/AbstractOperations.h index f71e1dac1bb..30ca7ed6e38 100644 --- a/Userland/Libraries/LibWeb/Streams/AbstractOperations.h +++ b/Userland/Libraries/LibWeb/Streams/AbstractOperations.h @@ -15,6 +15,7 @@ #include #include #include +#include namespace Web::Streams { @@ -61,8 +62,8 @@ void readable_stream_reader_generic_release(ReadableStreamGenericReaderMixin&); void readable_stream_default_reader_error_read_requests(ReadableStreamDefaultReader&, JS::Value error); void readable_stream_byob_reader_error_read_into_requests(ReadableStreamBYOBReader&, JS::Value error); JS::Value readable_byte_stream_controller_convert_pull_into_descriptor(JS::Realm&, PullIntoDescriptor const&); -void readable_byte_stream_controller_pull_into(ReadableByteStreamController&, WebIDL::ArrayBufferView&, ReadIntoRequest&); -void readable_stream_byob_reader_read(ReadableStreamBYOBReader&, WebIDL::ArrayBufferView&, ReadIntoRequest&); +void readable_byte_stream_controller_pull_into(ReadableByteStreamController&, WebIDL::ArrayBufferView&, u64 min, ReadIntoRequest&); +void readable_stream_byob_reader_read(ReadableStreamBYOBReader&, WebIDL::ArrayBufferView&, u64 min, ReadIntoRequest&); void readable_byte_stream_controller_fill_head_pull_into_descriptor(ReadableByteStreamController const&, u64 size, PullIntoDescriptor&); void readable_stream_default_reader_read(ReadableStreamDefaultReader&, ReadRequest&); diff --git a/Userland/Libraries/LibWeb/Streams/ReadableByteStreamController.cpp b/Userland/Libraries/LibWeb/Streams/ReadableByteStreamController.cpp index ea98ee8e3d9..720f8930dd5 100644 --- a/Userland/Libraries/LibWeb/Streams/ReadableByteStreamController.cpp +++ b/Userland/Libraries/LibWeb/Streams/ReadableByteStreamController.cpp @@ -155,6 +155,7 @@ void ReadableByteStreamController::pull_steps(JS::NonnullGCPtr read .byte_offset = 0, .byte_length = *m_auto_allocate_chunk_size, .bytes_filled = 0, + .minimum_fill = 1, .element_size = 1, .view_constructor = *realm.intrinsics().uint8_array_constructor(), .reader_type = ReaderType::Default, diff --git a/Userland/Libraries/LibWeb/Streams/ReadableByteStreamController.h b/Userland/Libraries/LibWeb/Streams/ReadableByteStreamController.h index 52884bf115b..72bc8a221a6 100644 --- a/Userland/Libraries/LibWeb/Streams/ReadableByteStreamController.h +++ b/Userland/Libraries/LibWeb/Streams/ReadableByteStreamController.h @@ -41,6 +41,10 @@ struct PullIntoDescriptor { // A nonnegative integer number of bytes that have been written into the buffer so far u64 bytes_filled; + // https://streams.spec.whatwg.org/#pull-into-descriptor-minimum-fill + // A positive integer representing the minimum number of bytes that must be written into the buffer before the associated read() request may be fulfilled. By default, this equals the element size. + u64 minimum_fill; + // https://streams.spec.whatwg.org/#pull-into-descriptor-element-size // A positive integer representing the number of bytes that can be written into the buffer at a time, using views of the type described by the view constructor u64 element_size; diff --git a/Userland/Libraries/LibWeb/Streams/ReadableStreamBYOBReader.cpp b/Userland/Libraries/LibWeb/Streams/ReadableStreamBYOBReader.cpp index 185e273efc1..3861c1f5926 100644 --- a/Userland/Libraries/LibWeb/Streams/ReadableStreamBYOBReader.cpp +++ b/Userland/Libraries/LibWeb/Streams/ReadableStreamBYOBReader.cpp @@ -107,7 +107,7 @@ private: JS_DEFINE_ALLOCATOR(BYOBReaderReadIntoRequest); // https://streams.spec.whatwg.org/#byob-reader-read -JS::NonnullGCPtr ReadableStreamBYOBReader::read(JS::Handle& view) +JS::NonnullGCPtr ReadableStreamBYOBReader::read(JS::Handle& view, ReadableStreamBYOBReaderReadOptions options) { auto& realm = this->realm(); @@ -129,16 +129,42 @@ JS::NonnullGCPtr ReadableStreamBYOBReader::read(JS::Handleis_typed_array_base()) { + auto const& typed_array = *view->bufferable_object().get>(); + + // 1. If options["min"] > view.[[ArrayLength]], return a promise rejected with a RangeError exception. + if (options.min > typed_array.array_length().length()) { + WebIDL::SimpleException exception { WebIDL::SimpleExceptionType::RangeError, "options[\"min\"] cannot be larger than the length of the view."sv }; + return WebIDL::create_rejected_promise_from_exception(realm, move(exception)); + } + } + + // 6. Otherwise (i.e., it is a DataView), + if (view->is_data_view()) { + // 1. If options["min"] > view.[[ByteLength]], return a promise rejected with a RangeError exception. + if (options.min > view->byte_length()) { + WebIDL::SimpleException exception { WebIDL::SimpleExceptionType::RangeError, "options[\"min\"] cannot be larger than the length of the view."sv }; + return WebIDL::create_rejected_promise_from_exception(realm, move(exception)); + } + } + + // 7. If this.[[stream]] is undefined, return a promise rejected with a TypeError exception. if (!m_stream) { WebIDL::SimpleException exception { WebIDL::SimpleExceptionType::TypeError, "Cannot read from an empty stream"sv }; return WebIDL::create_rejected_promise_from_exception(realm, move(exception)); } - // 5. Let promise be a new promise. + // 8. Let promise be a new promise. auto promise_capability = WebIDL::create_promise(realm); - // 6. Let readIntoRequest be a new read-into request with the following items: + // 9. Let readIntoRequest be a new read-into request with the following items: // chunk steps, given chunk // Resolve promise with «[ "value" → chunk, "done" → false ]». // close steps, given chunk @@ -147,10 +173,10 @@ JS::NonnullGCPtr ReadableStreamBYOBReader::read(JS::Handle(realm, promise_capability); - // 7. Perform ! ReadableStreamBYOBReaderRead(this, view, readIntoRequest). - readable_stream_byob_reader_read(*this, *view, *read_into_request); + // 10. Perform ! ReadableStreamBYOBReaderRead(this, view, options["min"], readIntoRequest). + readable_stream_byob_reader_read(*this, *view, options.min, *read_into_request); - // 8. Return promise. + // 11. Return promise. return JS::NonnullGCPtr { verify_cast(*promise_capability->promise()) }; } } diff --git a/Userland/Libraries/LibWeb/Streams/ReadableStreamBYOBReader.h b/Userland/Libraries/LibWeb/Streams/ReadableStreamBYOBReader.h index 5f34fbf58be..264ef5437f8 100644 --- a/Userland/Libraries/LibWeb/Streams/ReadableStreamBYOBReader.h +++ b/Userland/Libraries/LibWeb/Streams/ReadableStreamBYOBReader.h @@ -13,9 +13,15 @@ #include #include #include +#include namespace Web::Streams { +// https://streams.spec.whatwg.org/#dictdef-readablestreambyobreaderreadoptions +struct ReadableStreamBYOBReaderReadOptions { + WebIDL::UnsignedLongLong min = 1; +}; + // https://streams.spec.whatwg.org/#read-into-request class ReadIntoRequest : public JS::Cell { JS_CELL(ReadIntoRequest, JS::Cell); @@ -45,7 +51,7 @@ public: virtual ~ReadableStreamBYOBReader() override = default; - JS::NonnullGCPtr read(JS::Handle&); + JS::NonnullGCPtr read(JS::Handle&, ReadableStreamBYOBReaderReadOptions options = {}); void release_lock(); diff --git a/Userland/Libraries/LibWeb/Streams/ReadableStreamBYOBReader.idl b/Userland/Libraries/LibWeb/Streams/ReadableStreamBYOBReader.idl index 4e37bad9b2d..816392f1b92 100644 --- a/Userland/Libraries/LibWeb/Streams/ReadableStreamBYOBReader.idl +++ b/Userland/Libraries/LibWeb/Streams/ReadableStreamBYOBReader.idl @@ -7,8 +7,12 @@ interface ReadableStreamBYOBReader { constructor(ReadableStream stream); - Promise read(ArrayBufferView view); + Promise read(ArrayBufferView view, optional ReadableStreamBYOBReaderReadOptions options = {}); undefined releaseLock(); }; ReadableStreamBYOBReader includes ReadableStreamGenericReader; + +dictionary ReadableStreamBYOBReaderReadOptions { + [EnforceRange] unsigned long long min = 1; +};