Skip to content

Commit

Permalink
Add Request and Response blob methods (#177)
Browse files Browse the repository at this point in the history
* Implement Request#blob and Response#blob methods

* Refactor Blob internal buffer.

This patch allows creating buffer from owned memory by simply
transfering the ownership rather than copying memory.

* Remove outdated comment

* Add blob body input

* Implement structured-clone for Blob
  • Loading branch information
andreiltd authored Dec 6, 2024
1 parent 59ead96 commit c61b935
Show file tree
Hide file tree
Showing 21 changed files with 211 additions and 112 deletions.
62 changes: 38 additions & 24 deletions builtins/web/blob.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
#include "builtin.h"
#include "encode.h"
#include "extension-api.h"
#include "mozilla/UniquePtr.h"
#include "js/UniquePtr.h"
#include "rust-encoding.h"
#include "streams/native-stream-source.h"

#include "mozilla/UniquePtr.h"
#include "js/ArrayBuffer.h"
#include "js/Conversions.h"
#include "js/experimental/TypedData.h"
#include "js/HashTable.h"
#include "js/Stream.h"
#include "js/TypeDecls.h"
#include "js/Value.h"
Expand Down Expand Up @@ -149,6 +151,8 @@ namespace builtins {
namespace web {
namespace blob {

using js::Vector;

const JSFunctionSpec Blob::static_methods[] = {
JS_FS_END,
};
Expand Down Expand Up @@ -247,15 +251,15 @@ JSObject *Blob::data_to_owned_array_buffer(JSContext *cx, HandleObject self) {
JSObject *Blob::data_to_owned_array_buffer(JSContext *cx, HandleObject self, size_t offset,
size_t size, size_t *bytes_read) {
auto blob = Blob::blob(self);
auto blob_size = blob->size();
auto blob_size = blob->length();
*bytes_read = 0;

MOZ_ASSERT(offset <= blob_size);

size_t available_bytes = blob_size - offset;
size_t read_size = std::min(size, available_bytes);

auto span = std::span<uint8_t>(blob->data() + offset, read_size);
auto span = std::span<uint8_t>(blob->begin() + offset, read_size);
auto array_buffer = new_array_buffer_from_span(cx, span);
if (!array_buffer) {
return nullptr;
Expand Down Expand Up @@ -319,7 +323,7 @@ bool Blob::slice(JSContext *cx, unsigned argc, JS::Value *vp) {
METHOD_HEADER(0)

auto src = Blob::blob(self);
int64_t size = src->size();
int64_t size = src->length();
int64_t start = 0;
int64_t end = size;

Expand Down Expand Up @@ -350,11 +354,14 @@ bool Blob::slice(JSContext *cx, unsigned argc, JS::Value *vp) {
start = (start < 0) ? std::max((size + start), 0LL) : std::min(start, size);
end = (end < 0) ? std::max((size + end), 0LL) : std::min(end, size);

auto dst = (end - start > 0)
? std::make_unique<std::vector<uint8_t>>(src->begin() + start, src->begin() + end)
: std::make_unique<std::vector<uint8_t>>();
auto slice_len = std::max(end - start, 0LL);
auto dst = (slice_len > 0) ? UniqueChars(js_pod_malloc<char>(slice_len)) : nullptr;

if (dst) {
std::copy(src->begin() + start, src->begin() + end, dst.get());
}

JS::RootedObject new_blob(cx, create(cx, std::move(dst), contentType));
JS::RootedObject new_blob(cx, create(cx, std::move(dst), slice_len, contentType));
if (!new_blob) {
return false;
}
Expand All @@ -376,7 +383,7 @@ bool Blob::stream(JSContext *cx, unsigned argc, JS::Value *vp) {

auto readers = Blob::readers(self);
auto blob = Blob::blob(self);
auto span = std::span<uint8_t>(blob->data(), blob->size());
auto span = std::span<uint8_t>(blob->begin(), blob->length());

if (!readers->put(source, BlobReader(span))) {
return false;
Expand Down Expand Up @@ -408,7 +415,7 @@ bool Blob::text(JSContext *cx, unsigned argc, JS::Value *vp) {
auto decoder = jsencoding::encoding_new_decoder_with_bom_removal(encoding);
MOZ_ASSERT(decoder);

auto src_len = src->size();
auto src_len = src->length();
auto dst_len = jsencoding::decoder_max_utf16_buffer_length(decoder, src_len);

JS::UniqueTwoByteChars dst(new char16_t[dst_len + 1]);
Expand All @@ -420,7 +427,7 @@ bool Blob::text(JSContext *cx, unsigned argc, JS::Value *vp) {
bool had_replacements;
auto dst_data = reinterpret_cast<uint16_t *>(dst.get());

jsencoding::decoder_decode_to_utf16(decoder, src->data(), &src_len, dst_data, &dst_len, true,
jsencoding::decoder_decode_to_utf16(decoder, src->begin(), &src_len, dst_data, &dst_len, true,
&had_replacements);

JS::RootedString str(cx, JS_NewUCString(cx, std::move(dst), dst_len));
Expand Down Expand Up @@ -474,17 +481,17 @@ bool Blob::stream_pull(JSContext *cx, JS::CallArgs args, JS::HandleObject source
return true;
}

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

MOZ_ASSERT(blob);
return blob;
}

size_t Blob::blob_size(JSObject *self) {
return blob(self)->size();
return blob(self)->length();
}

JSString *Blob::type(JSObject *self) {
Expand Down Expand Up @@ -515,14 +522,16 @@ bool Blob::append_value(JSContext *cx, HandleObject self, HandleValue val) {

if (Blob::is_instance(obj)) {
auto src = Blob::blob(obj);
blob->insert(blob->end(), src->begin(), src->end());
return true;
return blob->append(src->begin(), src->end());
} else if (JS_IsArrayBufferViewObject(obj) || JS::IsArrayBufferObject(obj)) {
auto span = value_to_buffer(cx, val, "Blob Parts");
if (span.has_value()) {
blob->insert(blob->end(), span->begin(), span->end());
auto src = span->data();
auto len = span->size();
return blob->append(src, src + len);
} else {
return true;
}
return true;
}
} else if (val.isString()) {
auto chars = core::encode(cx, val);
Expand All @@ -534,14 +543,13 @@ bool Blob::append_value(JSContext *cx, HandleObject self, HandleValue val) {
auto converted = convert_line_endings_to_native(chars);
auto src = converted.data();
auto len = converted.length();
blob->insert(blob->end(), src, src + len);
return blob->append(src, src + len);

} else {
auto src = chars.ptr.get();
auto len = chars.len;
blob->insert(blob->end(), src, src + len);
return blob->append(src, src + len);
}
return true;
}

// FALLBACK: if we ever get here convert, to string and call append again
Expand Down Expand Up @@ -645,13 +653,19 @@ bool Blob::init_options(JSContext *cx, HandleObject self, HandleValue initv) {
return true;
}

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

SetReservedSlot(self, static_cast<uint32_t>(Slots::Data), JS::PrivateValue(data.release()));
auto blob = new ByteBuffer;
if (data != nullptr) {
// Take the ownership of given data.
blob->replaceRawBuffer(reinterpret_cast<uint8_t *>(data.release()), data_len);
}

SetReservedSlot(self, static_cast<uint32_t>(Slots::Data), JS::PrivateValue(blob));
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), JS::PrivateValue(new ReadersMap));
Expand All @@ -669,9 +683,9 @@ bool Blob::constructor(JSContext *cx, unsigned argc, JS::Value *vp) {
return false;
}

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::Data), JS::PrivateValue(new ByteBuffer));
SetReservedSlot(self, static_cast<uint32_t>(Slots::Readers), JS::PrivateValue(new ReadersMap));

// Walk the blob parts and append them to the blob's buffer.
Expand Down
8 changes: 4 additions & 4 deletions builtins/web/blob.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "js/AllocPolicy.h"
#include "js/GCHashTable.h"
#include "js/TypeDecls.h"
#include "js/Vector.h"

namespace builtins {
namespace web {
Expand Down Expand Up @@ -53,10 +54,9 @@ class Blob : public TraceableBuiltinImpl<Blob> {
enum Slots { Data, Type, Endings, Readers, Count };
enum LineEndings { Transparent, Native };

using ByteBuffer = std::vector<uint8_t>;
using HeapObj = Heap<JSObject *>;
using ReadersMap =
JS::GCHashMap<HeapObj, BlobReader, js::StableCellHasher<HeapObj>, js::SystemAllocPolicy>;
using ByteBuffer = js::Vector<uint8_t, 0, js::SystemAllocPolicy>;
using ReadersMap = JS::GCHashMap<HeapObj, BlobReader, js::StableCellHasher<HeapObj>, js::SystemAllocPolicy>;

static ReadersMap *readers(JSObject *self);
static ByteBuffer *blob(JSObject *self);
Expand All @@ -75,7 +75,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, HandleString type);
static JSObject *create(JSContext *cx, UniqueChars data, size_t data_len, HandleString type);

static bool init_class(JSContext *cx, HandleObject global);
static bool constructor(JSContext *cx, unsigned argc, Value *vp);
Expand Down
34 changes: 33 additions & 1 deletion builtins/web/fetch/request-response.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "request-response.h"

#include "../blob.h"
#include "../streams/native-stream-source.h"
#include "../streams/transform-stream.h"
#include "../url.h"
Expand All @@ -8,6 +9,7 @@
#include "extension-api.h"
#include "fetch_event.h"
#include "host_api.h"
#include "js/String.h"
#include "picosha2.h"

#include "js/Array.h"
Expand All @@ -25,6 +27,8 @@
#include "js/experimental/TypedData.h"
#pragma clang diagnostic pop

using builtins::web::blob::Blob;

namespace builtins::web::streams {

bool NativeStreamSource::stream_is_body(JSContext *cx, JS::HandleObject stream) {
Expand Down Expand Up @@ -288,6 +292,7 @@ bool RequestOrResponse::extract_body(JSContext *cx, JS::HandleObject self,
// We currently support five types of body inputs:
// - byte sequence
// - buffer source
// - Blob
// - USV strings
// - URLSearchParams
// - ReadableStream
Expand All @@ -296,8 +301,24 @@ bool RequestOrResponse::extract_body(JSContext *cx, JS::HandleObject self,
// TODO: Support the other possible inputs to Body.

JS::RootedObject body_obj(cx, body_val.isObject() ? &body_val.toObject() : nullptr);
host_api::HostString host_type_str;

if (Blob::is_instance(body_obj)) {
RootedValue stream(cx);
if (!Call(cx, body_obj, "stream", HandleValueArray::empty(), &stream)) {
return false;
}

MOZ_ASSERT(stream.isObject());
JS_SetReservedSlot(self, static_cast<uint32_t>(RequestOrResponse::Slots::BodyStream), stream);

if (body_obj && JS::IsReadableStream(body_obj)) {
JS::RootedString type_str(cx, Blob::type(body_obj));
if (JS::GetStringLength(type_str) > 0) {
host_type_str = core::encode(cx, type_str);
MOZ_ASSERT(host_type_str);
content_type = host_type_str.ptr.get();
}
} else if (body_obj && JS::IsReadableStream(body_obj)) {
if (RequestOrResponse::body_unusable(cx, body_obj)) {
return api::throw_error(cx, FetchErrors::BodyStreamUnusable);
}
Expand Down Expand Up @@ -539,6 +560,15 @@ bool RequestOrResponse::parse_body(JSContext *cx, JS::HandleObject self, JS::Uni
}
static_cast<void>(buf.release());
result.setObject(*array_buffer);
} else if constexpr (result_type == RequestOrResponse::BodyReadResult::Blob) {
JS::RootedString contentType(cx, JS_GetEmptyString(cx));
JS::RootedObject blob(cx, blob::Blob::create(cx, std::move(buf), len, contentType));

if (!blob) {
return RejectPromiseWithPendingError(cx, result_promise);
}

result.setObject(*blob);
} else {
JS::RootedString text(cx, JS_NewStringCopyUTF8N(cx, JS::UTF8Chars(buf.get(), len)));
if (!text) {
Expand Down Expand Up @@ -1303,6 +1333,7 @@ const JSPropertySpec Request::static_properties[] = {
const JSFunctionSpec Request::methods[] = {
JS_FN("arrayBuffer", Request::bodyAll<RequestOrResponse::BodyReadResult::ArrayBuffer>, 0,
JSPROP_ENUMERATE),
JS_FN("blob", Request::bodyAll<RequestOrResponse::BodyReadResult::Blob>, 0, JSPROP_ENUMERATE),
JS_FN("json", Request::bodyAll<RequestOrResponse::BodyReadResult::JSON>, 0, JSPROP_ENUMERATE),
JS_FN("text", Request::bodyAll<RequestOrResponse::BodyReadResult::Text>, 0, JSPROP_ENUMERATE),
JS_FN("clone", Request::clone, 0, JSPROP_ENUMERATE),
Expand Down Expand Up @@ -2294,6 +2325,7 @@ const JSPropertySpec Response::static_properties[] = {
const JSFunctionSpec Response::methods[] = {
JS_FN("arrayBuffer", bodyAll<RequestOrResponse::BodyReadResult::ArrayBuffer>, 0,
JSPROP_ENUMERATE),
JS_FN("blob", bodyAll<RequestOrResponse::BodyReadResult::Blob>, 0, JSPROP_ENUMERATE),
JS_FN("json", bodyAll<RequestOrResponse::BodyReadResult::JSON>, 0, JSPROP_ENUMERATE),
JS_FN("text", bodyAll<RequestOrResponse::BodyReadResult::Text>, 0, JSPROP_ENUMERATE),
JS_FS_END,
Expand Down
1 change: 1 addition & 0 deletions builtins/web/fetch/request-response.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class RequestOrResponse final {

enum class BodyReadResult {
ArrayBuffer,
Blob,
JSON,
Text,
};
Expand Down
Loading

0 comments on commit c61b935

Please sign in to comment.