Skip to content

Add Blob builtin implementation #169

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

Merged
merged 42 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
faa8e6e
Add Blob builtin implementation
andreiltd Oct 23, 2024
7016066
Check the response status when fetching the file.
andreiltd Oct 24, 2024
44a4036
Add blob test expectation files
andreiltd Oct 24, 2024
8270c64
Pass more tests.
andreiltd Oct 29, 2024
1024ec5
Pass more tests.
andreiltd Oct 29, 2024
48df322
Fix slice tests
andreiltd Oct 30, 2024
90d68d4
Fix default options tests
andreiltd Oct 31, 2024
c64f6a1
Fix options evaluation order tests
andreiltd Nov 4, 2024
873c9ef
Fix calling ToString on blob parts
andreiltd Nov 4, 2024
09c8a17
Simplify appending string types
andreiltd Nov 4, 2024
715d9e0
Do not overwrite active exceptions in constructor
andreiltd Nov 4, 2024
5bc873f
Use UTF-16 endcoder from rust-encoding for text method
andreiltd Nov 5, 2024
9a7798a
Fix release build
andreiltd Nov 6, 2024
ce08991
Hack Blob#stream support.
andreiltd Nov 7, 2024
3b054c5
Merge remote-tracking branch 'upstream/main' into blob-builtin
andreiltd Nov 7, 2024
90bb1ad
Close the readable stream after we mark id done
andreiltd Nov 8, 2024
3eb21b8
Modify Blob stream to read data in chunks
andreiltd Nov 12, 2024
18e03f8
Do not call cancel on closing stream
andreiltd Nov 12, 2024
3f3c0d9
Refactor stream reader methods
andreiltd Nov 14, 2024
fc9131d
Ensure separate stream reader states
andreiltd Nov 15, 2024
421b3a6
Update the constructor length
andreiltd Nov 15, 2024
ecfb0c4
Fix initialization with a Uint8Array object
andreiltd Nov 18, 2024
996d85d
Implement native line endings conversion
andreiltd Nov 18, 2024
5a56037
Apply code review suggestions
andreiltd Nov 26, 2024
504fde6
Catch readAll exception in the blob test
andreiltd Nov 26, 2024
a0cf051
Print assertion error
andreiltd Nov 26, 2024
0c349f0
Add TraceableBuiltinImpl class
andreiltd Nov 27, 2024
417ff38
Use GCHashTable for storing the readers
andreiltd Nov 28, 2024
7e3be60
Tweak a blob test
andreiltd Nov 28, 2024
52bd455
Print diff content on test failure
andreiltd Nov 28, 2024
1358e94
Wait for interval to finish in the blob test.
andreiltd Nov 28, 2024
924edb9
Address code review comments.
andreiltd Nov 28, 2024
745f80c
Store ReadableDefaultStream in NativeStream slot
andreiltd Nov 29, 2024
518816a
Initialize underlying source slots before creating a default stream
andreiltd Dec 2, 2024
e414aa0
Check if readable is object in transform-stream
andreiltd Dec 2, 2024
16aa50d
Fix slot getters
andreiltd Dec 2, 2024
d755a52
Add comment explaining order of initialization in native stream source
andreiltd Dec 2, 2024
bd82b0e
Make slot getters to use JSObject instead of HandleObject
andreiltd Dec 2, 2024
8a18cc5
Runt clang-format
andreiltd Dec 2, 2024
2d48a32
Enable stream assert for testing
andreiltd Dec 2, 2024
4f6c47b
Remove default_stream method from NativeStreamSource
andreiltd Dec 2, 2024
ca68060
Simplify blob_size method
andreiltd Dec 5, 2024
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
110 changes: 67 additions & 43 deletions builtins/web/blob.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
#include "js/TypeDecls.h"
#include "js/Value.h"

#include <regex>

namespace {

static api::Engine *ENGINE;
Expand Down Expand Up @@ -108,16 +106,41 @@ JSString *normalize_type(JSContext *cx, HandleValue value) {

// https://w3c.github.io/FileAPI/#convert-line-endings-to-native
std::string convert_line_endings_to_native(std::string_view s) {
std::string native_line_ending = "\n";
std::string native_line_ending = "\n";
#ifdef _WIN32
native_line_ending = "\r\n";
native_line_ending = "\r\n";
#endif

std::string result;
std::regex re(R"(\r\n|\r|\n)");
std::regex_replace(std::back_inserter(result), s.begin(), s.end(), re, native_line_ending);
std::string result;
result.reserve(s.size());

size_t i = 0;
while (i < s.size()) {
switch (s[i]) {
case '\r': {
if (i + 1 < s.size() && s[i + 1] == '\n') {
result.append(native_line_ending);
i += 2;
} else {
result.append(native_line_ending);
i += 1;
}
break;
}
case '\n': {
result.append(native_line_ending);
i += 1;
break;
}
default: {
result.push_back(s[i]);
i += 1;
break;
}
}
}

return result;
return result;
}

} // anonymous namespace
Expand Down Expand Up @@ -164,7 +187,7 @@ class StreamTask final : public api::AsyncTask {
[[nodiscard]] bool run(api::Engine *engine) override {
JSContext *cx = engine->cx();
RootedObject owner(cx, streams::NativeStreamSource::owner(source_));
RootedObject controller(cx, streams::NativeStreamSource::controller(source_));
RootedObject stream(cx, streams::NativeStreamSource::default_stream(source_));
RootedValue ret(cx);

auto readers = Blob::readers(owner);
Expand All @@ -178,7 +201,7 @@ class StreamTask final : public api::AsyncTask {
auto chunk_size = chunk.size();

if (chunk.empty()) {
if (!JS::Call(cx, controller, "close", HandleValueArray::empty(), &ret)) {
if (!JS::ReadableStreamClose(cx, stream)) {
return false;
}

Expand All @@ -196,9 +219,9 @@ class StreamTask final : public api::AsyncTask {
return false;
}

RootedValueArray<1> enqueue_args(cx);
enqueue_args[0].setObject(*bytes_buffer);
if (!JS::Call(cx, controller, "enqueue", enqueue_args, &ret)) {
RootedValue enqueue_val(cx);
enqueue_val.setObject(*bytes_buffer);
if (!JS::ReadableStreamEnqueue(cx, stream, enqueue_val)) {
return false;
}

Expand All @@ -211,7 +234,7 @@ class StreamTask final : public api::AsyncTask {
return true;
}

void trace(JSTracer *trc) override { TraceEdge(trc, &source_, "source for future"); }
void trace(JSTracer *trc) override { TraceEdge(trc, &source_, "Source for Blob StreamTask"); }
};

JSObject *Blob::data_to_owned_array_buffer(JSContext *cx, HandleObject self) {
Expand Down Expand Up @@ -246,7 +269,7 @@ bool Blob::arrayBuffer(JSContext *cx, unsigned argc, JS::Value *vp) {

JS::RootedObject promise(cx, JS::NewPromiseObject(cx, nullptr));
if (!promise) {
return ReturnPromiseRejectedWithPendingError(cx, args);
return false;
}

args.rval().setObject(*promise);
Expand All @@ -268,7 +291,7 @@ bool Blob::bytes(JSContext *cx, unsigned argc, JS::Value *vp) {

JS::RootedObject promise(cx, JS::NewPromiseObject(cx, nullptr));
if (!promise) {
return ReturnPromiseRejectedWithPendingError(cx, args);
return false;
}

args.rval().setObject(*promise);
Expand Down Expand Up @@ -322,7 +345,7 @@ bool Blob::slice(JSContext *cx, unsigned argc, JS::Value *vp) {
}
}

// A negative value, is treated as an offset from the end of the Blob toward the beginning
// A negative value is treated as an offset from the end of the Blob toward the beginning.
start = (start < 0) ? std::max((size + start), 0LL) : std::min(start, size);
end = (end < 0) ? std::max((size + end), 0LL) : std::min(end, size);

Expand Down Expand Up @@ -358,12 +381,12 @@ bool Blob::stream(JSContext *cx, unsigned argc, JS::Value *vp) {
return false;
}

JS::RootedObject src_stream(cx, JS::NewReadableDefaultStreamObject(cx, source, nullptr, 0.0));
if (!src_stream) {
JS::RootedObject default_stream(cx, streams::NativeStreamSource::default_stream(native_stream));
if (!default_stream) {
return false;
}

args.rval().setObject(*src_stream);
args.rval().setObject(*default_stream);
return true;
}

Expand All @@ -372,7 +395,7 @@ bool Blob::text(JSContext *cx, unsigned argc, JS::Value *vp) {

JS::RootedObject promise(cx, JS::NewPromiseObject(cx, nullptr));
if (!promise) {
return ReturnPromiseRejectedWithPendingError(cx, args);
return false;
}

args.rval().setObject(*promise);
Expand Down Expand Up @@ -451,39 +474,35 @@ bool Blob::stream_pull(JSContext *cx, JS::CallArgs args, JS::HandleObject source
return true;
}

std::vector<uint8_t> *Blob::blob(JSObject *self) {
std::vector<uint8_t> *Blob::blob(HandleObject self) {
auto blob = static_cast<std::vector<uint8_t> *>(
JS::GetReservedSlot(self, static_cast<size_t>(Blob::Slots::Data)).toPrivate());

MOZ_ASSERT(blob);
return blob;
}

size_t Blob::blob_size(JSObject *self) {
size_t Blob::blob_size(HandleObject self) {
auto blob = static_cast<std::vector<uint8_t> *>(
JS::GetReservedSlot(self, static_cast<size_t>(Blob::Slots::Data)).toPrivate());

MOZ_ASSERT(blob);
return blob->size();
}

JSString *Blob::type(JSObject *self) {
auto type = static_cast<JSString *>(
JS::GetReservedSlot(self, static_cast<size_t>(Blob::Slots::Type)).toPrivate());

MOZ_ASSERT(type);
return type;
JSString *Blob::type(HandleObject self) {
return JS::GetReservedSlot(self, static_cast<size_t>(Blob::Slots::Type)).toString();
}

Blob::ReadersMap *Blob::readers(JSObject *self) {
Blob::ReadersMap *Blob::readers(HandleObject self) {
auto readers = static_cast<ReadersMap *>(
JS::GetReservedSlot(self, static_cast<size_t>(Blob::Slots::Readers)).toPrivate());

MOZ_ASSERT(readers);
return readers;
}

Blob::LineEndings Blob::line_endings(JSObject *self) {
Blob::LineEndings Blob::line_endings(HandleObject self) {
return static_cast<LineEndings>(
JS::GetReservedSlot(self, static_cast<size_t>(Blob::Slots::Endings)).toInt32());
}
Expand Down Expand Up @@ -620,22 +639,22 @@ bool Blob::init_options(JSContext *cx, HandleObject self, HandleValue initv) {
if (!type_str) {
return false;
}
SetReservedSlot(self, static_cast<uint32_t>(Slots::Type), PrivateValue(type_str));
SetReservedSlot(self, static_cast<uint32_t>(Slots::Type), JS::StringValue(type_str));
}

return true;
}

JSObject *Blob::create(JSContext *cx, std::unique_ptr<Blob::ByteBuffer> data, JSString *type) {
JSObject *Blob::create(JSContext *cx, std::unique_ptr<Blob::ByteBuffer> data, HandleString type) {
JSObject *self = JS_NewObjectWithGivenProto(cx, &class_, proto_obj);
if (!self) {
return nullptr;
}

SetReservedSlot(self, static_cast<uint32_t>(Slots::Data), PrivateValue(data.release()));
SetReservedSlot(self, static_cast<uint32_t>(Slots::Type), PrivateValue(type));
SetReservedSlot(self, static_cast<uint32_t>(Slots::Data), JS::PrivateValue(data.release()));
SetReservedSlot(self, static_cast<uint32_t>(Slots::Type), JS::StringValue(type));
SetReservedSlot(self, static_cast<uint32_t>(Slots::Endings), JS::Int32Value(LineEndings::Transparent));
SetReservedSlot(self, static_cast<uint32_t>(Slots::Readers), PrivateValue(new ReadersMap));
SetReservedSlot(self, static_cast<uint32_t>(Slots::Readers), JS::PrivateValue(new ReadersMap));
return self;
}

Expand All @@ -650,12 +669,12 @@ bool Blob::constructor(JSContext *cx, unsigned argc, JS::Value *vp) {
return false;
}

SetReservedSlot(self, static_cast<uint32_t>(Slots::Data), PrivateValue(new std::vector<uint8_t>()));
SetReservedSlot(self, static_cast<uint32_t>(Slots::Type), PrivateValue(JS_GetEmptyString(cx)));
SetReservedSlot(self, static_cast<uint32_t>(Slots::Data), JS::PrivateValue(new std::vector<uint8_t>()));
SetReservedSlot(self, static_cast<uint32_t>(Slots::Type), JS_GetEmptyStringValue(cx));
SetReservedSlot(self, static_cast<uint32_t>(Slots::Endings), JS::Int32Value(LineEndings::Transparent));
SetReservedSlot(self, static_cast<uint32_t>(Slots::Readers), PrivateValue(new ReadersMap));
SetReservedSlot(self, static_cast<uint32_t>(Slots::Readers), JS::PrivateValue(new ReadersMap));

// walk the blob parts and write it to concatenated buffer
// Walk the blob parts and append them to the blob's buffer.
if (blobParts.isNull()) {
return api::throw_error(cx, api::Errors::TypeError, "Blob.constructor", "blobParts", "be an object");
}
Expand All @@ -677,19 +696,24 @@ bool Blob::init_class(JSContext *cx, JS::HandleObject global) {
}

void Blob::finalize(JS::GCContext *gcx, JSObject *self) {
auto blob = Blob::blob(self);
auto blob = static_cast<std::vector<uint8_t> *>(
JS::GetReservedSlot(self, static_cast<size_t>(Blob::Slots::Data)).toPrivate());

if (blob) {
free(blob);
}

auto readers = Blob::readers(self);
auto readers = static_cast<ReadersMap *>(
JS::GetReservedSlot(self, static_cast<size_t>(Blob::Slots::Readers)).toPrivate());
if (readers) {
free(readers);
}
}

void Blob::trace(JSTracer* trc, JSObject *self) {
auto readers = Blob::readers(self);
auto readers = static_cast<ReadersMap *>(
JS::GetReservedSlot(self, static_cast<size_t>(Blob::Slots::Readers)).toPrivate());

readers->trace(trc);
}

Expand Down
12 changes: 6 additions & 6 deletions builtins/web/blob.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ class Blob : public TraceableBuiltinImpl<Blob> {
using HeapObj = Heap<JSObject *>;
using ReadersMap = JS::GCHashMap<HeapObj, BlobReader, js::StableCellHasher<HeapObj>, js::SystemAllocPolicy>;

static ReadersMap *readers(JSObject *self);
static ByteBuffer *blob(JSObject *self);
static size_t blob_size(JSObject *self);
static JSString *type(JSObject *self);
static LineEndings line_endings(JSObject *self);
static ReadersMap *readers(HandleObject self);
static ByteBuffer *blob(HandleObject self);
static size_t blob_size(HandleObject self);
static JSString *type(HandleObject self);
static LineEndings line_endings(HandleObject self);
static bool append_value(JSContext *cx, HandleObject self, HandleValue val);
static bool init_blob_parts(JSContext *cx, HandleObject self, HandleValue iterable);
static bool init_options(JSContext *cx, HandleObject self, HandleValue opts);
Expand All @@ -76,7 +76,7 @@ class Blob : public TraceableBuiltinImpl<Blob> {

static JSObject *data_to_owned_array_buffer(JSContext *cx, HandleObject self);
static JSObject *data_to_owned_array_buffer(JSContext* cx, HandleObject self, size_t offset, size_t size, size_t* bytes_read);
static JSObject *create(JSContext *cx, std::unique_ptr<ByteBuffer> data, JSString *type);
static JSObject *create(JSContext *cx, std::unique_ptr<ByteBuffer> data, HandleString type);

static bool init_class(JSContext *cx, HandleObject global);
static bool constructor(JSContext *cx, unsigned argc, Value *vp);
Expand Down
38 changes: 18 additions & 20 deletions builtins/web/fetch/request-response.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,15 @@ namespace builtins::web::fetch {

static api::Engine *ENGINE;

bool error_stream_controller_with_pending_exception(JSContext *cx, HandleObject controller) {
bool error_stream_controller_with_pending_exception(JSContext *cx, HandleObject stream) {
RootedValue exn(cx);
if (!JS_GetPendingException(cx, &exn))
return false;
JS_ClearPendingException(cx);

RootedValueArray<1> args(cx);
args[0].set(exn);
RootedValue r(cx);
return JS::Call(cx, controller, "error", args, &r);
RootedValue args(cx);
args.set(exn);
return JS::ReadableStreamError(cx, stream, args);
}

constexpr size_t HANDLE_READ_CHUNK_SIZE = 8192;
Expand All @@ -74,19 +73,18 @@ class BodyFutureTask final : public api::AsyncTask {
// MOZ_ASSERT(ready());
JSContext *cx = engine->cx();
RootedObject owner(cx, streams::NativeStreamSource::owner(body_source_));
RootedObject controller(cx, streams::NativeStreamSource::controller(body_source_));
RootedObject stream(cx, streams::NativeStreamSource::default_stream(body_source_));
auto body = RequestOrResponse::incoming_body_handle(owner);

auto read_res = body->read(HANDLE_READ_CHUNK_SIZE);
if (auto *err = read_res.to_err()) {
HANDLE_ERROR(cx, *err);
return error_stream_controller_with_pending_exception(cx, controller);
return error_stream_controller_with_pending_exception(cx, stream);
}

auto &chunk = read_res.unwrap();
if (chunk.done) {
RootedValue r(cx);
return Call(cx, controller, "close", HandleValueArray::empty(), &r);
return JS::ReadableStreamClose(cx, stream);
}

// We don't release control of chunk's data until after we've checked that
Expand All @@ -97,7 +95,7 @@ class BodyFutureTask final : public api::AsyncTask {
cx, JS::NewArrayBufferWithContents(cx, bytes.len, bytes.ptr.get(),
JS::NewArrayBufferOutOfMemory::CallerMustFreeMemory));
if (!buffer) {
return error_stream_controller_with_pending_exception(cx, controller);
return error_stream_controller_with_pending_exception(cx, stream);
}

// At this point `buffer` has taken full ownership of the chunk's data.
Expand All @@ -108,11 +106,10 @@ class BodyFutureTask final : public api::AsyncTask {
return false;
}

RootedValueArray<1> enqueue_args(cx);
enqueue_args[0].setObject(*byte_array);
RootedValue r(cx);
if (!JS::Call(cx, controller, "enqueue", enqueue_args, &r)) {
return error_stream_controller_with_pending_exception(cx, controller);
RootedValue enqueue_val(cx);
enqueue_val.setObject(*byte_array);
if (!JS::ReadableStreamEnqueue(cx, stream, enqueue_val)) {
return error_stream_controller_with_pending_exception(cx, stream);
}

return cancel(engine);
Expand Down Expand Up @@ -1121,17 +1118,18 @@ bool RequestOrResponse::maybe_stream_body(JSContext *cx, JS::HandleObject body_o
JSObject *RequestOrResponse::create_body_stream(JSContext *cx, JS::HandleObject owner) {
MOZ_ASSERT(!body_stream(owner));
MOZ_ASSERT(has_body(owner));

// Create a readable stream with a highwater mark of 0.0 to prevent an eager
// pull. With the default HWM of 1.0, the streams implementation causes a
// pull, which means we enqueue a read from the host handle, which we quite
// often have no interest in at all.
JS::RootedObject source(cx, streams::NativeStreamSource::create(
cx, owner, JS::UndefinedHandleValue, body_source_pull_algorithm,
body_source_cancel_algorithm));
if (!source)
return nullptr;

// Create a readable stream with a highwater mark of 0.0 to prevent an eager
// pull. With the default HWM of 1.0, the streams implementation causes a
// pull, which means we enqueue a read from the host handle, which we quite
// often have no interest in at all.
JS::RootedObject body_stream(cx, JS::NewReadableDefaultStreamObject(cx, source, nullptr, 0.0));
JS::RootedObject body_stream(cx, streams::NativeStreamSource::default_stream(source));
if (!body_stream) {
return nullptr;
}
Expand Down
Loading