Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LibWeb: Implement Blob::text and Blob::array_buffer to spec #19404

Merged
merged 6 commits into from
Jun 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Userland/Libraries/LibWeb/Bindings/ExceptionOrUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ struct ExtractExceptionOrValueType<WebIDL::ExceptionOr<void>> {
using Type = JS::Value;
};

}

ALWAYS_INLINE JS::Completion dom_exception_to_throw_completion(JS::VM& vm, auto&& exception)
{
return exception.visit(
Expand All @@ -81,8 +83,6 @@ ALWAYS_INLINE JS::Completion dom_exception_to_throw_completion(JS::VM& vm, auto&
});
}

}

template<typename T>
using ExtractExceptionOrValueType = typename Detail::ExtractExceptionOrValueType<T>::Type;

Expand All @@ -97,7 +97,7 @@ JS::ThrowCompletionOr<Ret> throw_dom_exception_if_needed(JS::VM& vm, F&& fn)
auto&& result = fn();

if (result.is_exception())
return Detail::dom_exception_to_throw_completion(vm, result.exception());
return dom_exception_to_throw_completion(vm, result.exception());

if constexpr (requires(T v) { v.value(); })
return result.value();
Expand Down
3 changes: 1 addition & 2 deletions Userland/Libraries/LibWeb/Fetch/FetchMethod.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ JS::NonnullGCPtr<JS::Promise> fetch(JS::VM& vm, RequestInfo const& input, Reques
// as arguments. If this throws an exception, reject p with it and return p.
auto exception_or_request_object = Request::construct_impl(realm, input, init);
if (exception_or_request_object.is_exception()) {
// FIXME: We should probably make this a public API?
auto throw_completion = Bindings::Detail::dom_exception_to_throw_completion(vm, exception_or_request_object.release_error());
auto throw_completion = Bindings::dom_exception_to_throw_completion(vm, exception_or_request_object.exception());
WebIDL::reject_promise(realm, promise_capability, *throw_completion.value());
return verify_cast<JS::Promise>(*promise_capability->promise().ptr());
}
Expand Down
67 changes: 46 additions & 21 deletions Userland/Libraries/LibWeb/FileAPI/Blob.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
#include <LibJS/Runtime/ArrayBuffer.h>
#include <LibJS/Runtime/Completion.h>
#include <LibJS/Runtime/TypedArray.h>
#include <LibTextCodec/Decoder.h>
#include <LibWeb/Bindings/BlobPrototype.h>
#include <LibWeb/Bindings/ExceptionOrUtils.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/FileAPI/Blob.h>
#include <LibWeb/Infra/Strings.h>
#include <LibWeb/Streams/AbstractOperations.h>
#include <LibWeb/Streams/ReadableStreamDefaultReader.h>
#include <LibWeb/WebIDL/AbstractOperations.h>

namespace Web::FileAPI {
Expand Down Expand Up @@ -322,39 +324,62 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> Blob::text()
auto& realm = this->realm();
auto& vm = realm.vm();

// FIXME: 1. Let stream be the result of calling get stream on this.
// FIXME: 2. Let reader be the result of getting a reader from stream. If that threw an exception, return a new promise rejected with that exception.
// 1. Let stream be the result of calling get stream on this.
auto stream = TRY(this->get_stream());

// 2. Let reader be the result of getting a reader from stream. If that threw an exception, return a new promise rejected with that exception.
auto reader_or_exception = acquire_readable_stream_default_reader(*stream);
if (reader_or_exception.is_exception()) {
auto throw_completion = Bindings::dom_exception_to_throw_completion(vm, reader_or_exception.exception());
auto promise_capability = WebIDL::create_rejected_promise(realm, *throw_completion.value());
return JS::NonnullGCPtr { verify_cast<JS::Promise>(*promise_capability->promise().ptr()) };
}
auto reader = reader_or_exception.release_value();

// FIXME: We still need to implement ReadableStream for this step to be fully valid.
// 3. Let promise be the result of reading all bytes from stream with reader
auto promise = JS::Promise::create(realm);
auto result = TRY(Bindings::throw_dom_exception_if_needed(vm, [&]() { return JS::PrimitiveString::create(vm, StringView { m_byte_buffer.bytes() }); }));
auto promise = TRY(reader->read_all_bytes_deprecated());

// 4. Return the result of transforming promise by a fulfillment handler that returns the result of running UTF-8 decode on its first argument.
promise->fulfill(result);
return promise;
return WebIDL::upon_fulfillment(*promise, [&](auto const& first_argument) -> WebIDL::ExceptionOr<JS::Value> {
auto const& object = first_argument.as_object();
VERIFY(is<JS::ArrayBuffer>(object));
auto const& buffer = static_cast<const JS::ArrayBuffer&>(object).buffer();

auto decoder = TextCodec::decoder_for("UTF-8"sv);
auto utf8_text = TRY_OR_THROW_OOM(vm, TextCodec::convert_input_to_utf8_using_given_decoder_unless_there_is_a_byte_order_mark(*decoder, buffer));
return JS::PrimitiveString::create(vm, move(utf8_text));
});
}

// https://w3c.github.io/FileAPI/#dom-blob-arraybuffer
JS::Promise* Blob::array_buffer()
WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> Blob::array_buffer()
{
// FIXME: 1. Let stream be the result of calling get stream on this.
// FIXME: 2. Let reader be the result of getting a reader from stream. If that threw an exception, return a new promise rejected with that exception.
auto& realm = this->realm();
auto& vm = realm.vm();

// FIXME: We still need to implement ReadableStream for this step to be fully valid.
// 3. Let promise be the result of reading all bytes from stream with reader.
auto promise = JS::Promise::create(realm());
auto buffer_result = JS::ArrayBuffer::create(realm(), m_byte_buffer.size());
if (buffer_result.is_error()) {
promise->reject(buffer_result.release_error().value().release_value());
return promise;
// 1. Let stream be the result of calling get stream on this.
auto stream = TRY(this->get_stream());

// 2. Let reader be the result of getting a reader from stream. If that threw an exception, return a new promise rejected with that exception.
auto reader_or_exception = acquire_readable_stream_default_reader(*stream);
if (reader_or_exception.is_exception()) {
auto throw_completion = Bindings::dom_exception_to_throw_completion(vm, reader_or_exception.exception());
auto promise_capability = WebIDL::create_rejected_promise(realm, *throw_completion.value());
return JS::NonnullGCPtr { verify_cast<JS::Promise>(*promise_capability->promise().ptr()) };
}
auto buffer = buffer_result.release_value();
buffer->buffer().overwrite(0, m_byte_buffer.data(), m_byte_buffer.size());
auto reader = reader_or_exception.release_value();

// 3. Let promise be the result of reading all bytes from stream with reader.
auto promise = TRY(reader->read_all_bytes_deprecated());

// 4. Return the result of transforming promise by a fulfillment handler that returns a new ArrayBuffer whose contents are its first argument.
promise->fulfill(buffer);
return promise;
return WebIDL::upon_fulfillment(*promise, [&](auto const& first_argument) -> WebIDL::ExceptionOr<JS::Value> {
auto const& object = first_argument.as_object();
VERIFY(is<JS::ArrayBuffer>(object));
auto const& buffer = static_cast<const JS::ArrayBuffer&>(object).buffer();

return JS::ArrayBuffer::create(realm, buffer);
});
}

}
2 changes: 1 addition & 1 deletion Userland/Libraries/LibWeb/FileAPI/Blob.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class Blob : public Bindings::PlatformObject {

WebIDL::ExceptionOr<JS::NonnullGCPtr<Streams::ReadableStream>> stream();
WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> text();
JS::Promise* array_buffer();
WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> array_buffer();

ReadonlyBytes bytes() const { return m_byte_buffer.bytes(); }

Expand Down
99 changes: 99 additions & 0 deletions Userland/Libraries/LibWeb/Streams/ReadableStreamDefaultReader.cpp
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
/*
* Copyright (c) 2023, Matthew Olsson <[email protected]>
* Copyright (c) 2023, Shannon Booth <[email protected]>
*
* SPDX-License-Identifier: BSD-2-Clause
*/

#include <LibJS/Heap/Heap.h>
#include <LibJS/Runtime/ArrayBuffer.h>
#include <LibJS/Runtime/Error.h>
#include <LibJS/Runtime/IteratorOperations.h>
#include <LibJS/Runtime/PromiseCapability.h>
#include <LibJS/Runtime/Realm.h>
#include <LibJS/Runtime/TypedArray.h>
#include <LibWeb/Bindings/ExceptionOrUtils.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/ReadableStreamDefaultReaderPrototype.h>
#include <LibWeb/Streams/AbstractOperations.h>
Expand Down Expand Up @@ -50,6 +54,62 @@ void ReadableStreamDefaultReader::visit_edges(Cell::Visitor& visitor)
ReadableStreamGenericReaderMixin::visit_edges(visitor);
}

// https://streams.spec.whatwg.org/#read-loop
ReadLoopReadRequest::ReadLoopReadRequest(JS::VM& vm, JS::Realm& realm, ReadableStreamDefaultReader& reader, SuccessSteps success_steps, FailureSteps failure_steps)
: m_vm(vm)
, m_realm(realm)
, m_reader(reader)
, m_success_steps(move(success_steps))
, m_failure_steps(move(failure_steps))
{
}

// chunk steps, given chunk
void ReadLoopReadRequest::on_chunk(JS::Value chunk)
{
// 1. If chunk is not a Uint8Array object, call failureSteps with a TypeError and abort these steps.
if (!chunk.is_object() || !is<JS::Uint8Array>(chunk.as_object())) {
auto exception = JS::TypeError::create(m_realm, "Chunk data is not Uint8Array"sv);
if (exception.is_error()) {
m_failure_steps(*exception.release_error().value());
return;
}

m_failure_steps(exception.value());
}

auto const& array = static_cast<JS::Uint8Array const&>(chunk.as_object());
auto const& buffer = array.viewed_array_buffer()->buffer();

// 2. Append the bytes represented by chunk to bytes.
m_bytes.append(buffer);

// FIXME: As the spec suggests, implement this non-recursively - instead of directly. It is not too big of a deal currently
// as we enqueue the entire blob buffer in one go, meaning that we only recurse a single time. Once we begin queuing
// up more than one chunk at a time, we may run into stack overflow problems.
//
// 3. Read-loop given reader, bytes, successSteps, and failureSteps.
auto maybe_error = readable_stream_default_reader_read(m_reader, *this);
if (maybe_error.is_exception()) {
auto throw_completion = Bindings::dom_exception_to_throw_completion(m_vm, maybe_error.exception());
m_failure_steps(*throw_completion.release_error().value());
}
}

// close steps
void ReadLoopReadRequest::on_close()
{
// 1. Call successSteps with bytes.
m_success_steps(m_bytes);
}

// error steps, given e
void ReadLoopReadRequest::on_error(JS::Value error)
{
// 1. Call failureSteps with e.
m_failure_steps(error);
}

class DefaultReaderReadRequest : public ReadRequest {
public:
DefaultReaderReadRequest(JS::Realm& realm, WebIDL::Promise& promise)
Expand Down Expand Up @@ -109,6 +169,45 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> ReadableStreamDefaultReader::
return JS::NonnullGCPtr { verify_cast<JS::Promise>(*promise_capability->promise()) };
}

// https://streams.spec.whatwg.org/#readablestreamdefaultreader-read-all-bytes
WebIDL::ExceptionOr<void> ReadableStreamDefaultReader::read_all_bytes(ReadLoopReadRequest::SuccessSteps success_steps, ReadLoopReadRequest::FailureSteps failure_steps)
{
auto& realm = this->realm();
auto& vm = realm.vm();

// 1. Let readRequest be a new read request with the following items:
// NOTE: items and steps in ReadLoopReadRequest.
auto read_request = adopt_ref(*new ReadLoopReadRequest(vm, realm, *this, move(success_steps), move(failure_steps)));

// 2. Perform ! ReadableStreamDefaultReaderRead(this, readRequest).
TRY(readable_stream_default_reader_read(*this, read_request));

return {};
}

// FIXME: This function is a promise-based wrapper around "read all bytes". The spec changed this function to not use promises
// in https://github.com/whatwg/streams/commit/f894acdd417926a2121710803cef593e15127964 - however, it seems that the
// FileAPI blob specification has not been updated to match, see: https://github.com/w3c/FileAPI/issues/187.
WebIDL::ExceptionOr<JS::NonnullGCPtr<WebIDL::Promise>> ReadableStreamDefaultReader::read_all_bytes_deprecated()
{
auto& realm = this->realm();

auto promise = WebIDL::create_promise(realm);

auto success_steps = [promise, &realm](ByteBuffer bytes) {
auto buffer = JS::ArrayBuffer::create(realm, move(bytes));
WebIDL::resolve_promise(realm, promise, buffer);
};

auto failure_steps = [promise, &realm](JS::Value error) {
WebIDL::reject_promise(realm, promise, error);
};

TRY(read_all_bytes(move(success_steps), move(failure_steps)));

return promise;
}

// https://streams.spec.whatwg.org/#default-reader-release-lock
WebIDL::ExceptionOr<void> ReadableStreamDefaultReader::release_lock()
{
Expand Down
29 changes: 29 additions & 0 deletions Userland/Libraries/LibWeb/Streams/ReadableStreamDefaultReader.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,31 @@ class ReadRequest : public RefCounted<ReadRequest> {
virtual void on_error(JS::Value error) = 0;
};

class ReadLoopReadRequest : public ReadRequest {
public:
// successSteps, which is an algorithm accepting a byte sequence
using SuccessSteps = JS::SafeFunction<void(ByteBuffer)>;

// failureSteps, which is an algorithm accepting a JavaScript value
using FailureSteps = JS::SafeFunction<void(JS::Value error)>;

ReadLoopReadRequest(JS::VM& vm, JS::Realm& realm, ReadableStreamDefaultReader& reader, SuccessSteps success_steps, FailureSteps failure_steps);

virtual void on_chunk(JS::Value chunk) override;

virtual void on_close() override;

virtual void on_error(JS::Value error) override;

private:
JS::VM& m_vm;
JS::Realm& m_realm;
ReadableStreamDefaultReader& m_reader;
ByteBuffer m_bytes;
SuccessSteps m_success_steps;
FailureSteps m_failure_steps;
};

// https://streams.spec.whatwg.org/#readablestreamdefaultreader
class ReadableStreamDefaultReader final
: public Bindings::PlatformObject
Expand All @@ -41,6 +66,10 @@ class ReadableStreamDefaultReader final
virtual ~ReadableStreamDefaultReader() override = default;

WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> read();

WebIDL::ExceptionOr<void> read_all_bytes(ReadLoopReadRequest::SuccessSteps, ReadLoopReadRequest::FailureSteps);
WebIDL::ExceptionOr<JS::NonnullGCPtr<WebIDL::Promise>> read_all_bytes_deprecated();

WebIDL::ExceptionOr<void> release_lock();

SinglyLinkedList<NonnullRefPtr<ReadRequest>>& read_requests() { return m_read_requests; }
Expand Down