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

Add File interface #181

Merged
merged 13 commits into from
Jan 10, 2025
90 changes: 58 additions & 32 deletions builtins/web/blob.cpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
#include "blob.h"
#include "file.h"
#include "builtin.h"
#include "encode.h"
#include "extension-api.h"
#include "js/UniquePtr.h"
#include "rust-encoding.h"
#include "streams/native-stream-source.h"

#include "mozilla/UniquePtr.h"
#include "js/UniquePtr.h"
#include "js/ArrayBuffer.h"
#include "js/Conversions.h"
#include "js/experimental/TypedData.h"
Expand Down Expand Up @@ -152,6 +152,19 @@ namespace web {
namespace blob {

using js::Vector;
using file::File;

#define DEFINE_BLOB_METHOD(name) \
bool Blob::name(JSContext *cx, unsigned argc, JS::Value *vp) { \
METHOD_HEADER(0) \
return name(cx, self, args.rval()); \
}

#define DEFINE_BLOB_METHOD_W_ARGS(name) \
bool Blob::name(JSContext *cx, unsigned argc, JS::Value *vp) { \
METHOD_HEADER(0) \
return name(cx, self, args, args.rval()); \
}

const JSFunctionSpec Blob::static_methods[] = {
JS_FS_END,
Expand Down Expand Up @@ -267,15 +280,19 @@ JSObject *Blob::data_to_owned_array_buffer(JSContext *cx, HandleObject self, siz
return array_buffer;
}

bool Blob::arrayBuffer(JSContext *cx, unsigned argc, JS::Value *vp) {
METHOD_HEADER(0)
DEFINE_BLOB_METHOD(arrayBuffer)
DEFINE_BLOB_METHOD(bytes)
DEFINE_BLOB_METHOD(stream)
DEFINE_BLOB_METHOD(text)
DEFINE_BLOB_METHOD_W_ARGS(slice)
tschneidereit marked this conversation as resolved.
Show resolved Hide resolved

bool Blob::arrayBuffer(JSContext *cx, HandleObject self, MutableHandleValue rval) {
JS::RootedObject promise(cx, JS::NewPromiseObject(cx, nullptr));
if (!promise) {
return false;
}

args.rval().setObject(*promise);
rval.setObject(*promise);

auto buffer = data_to_owned_array_buffer(cx, self);
if (!buffer) {
Expand All @@ -289,15 +306,13 @@ bool Blob::arrayBuffer(JSContext *cx, unsigned argc, JS::Value *vp) {
return true;
}

bool Blob::bytes(JSContext *cx, unsigned argc, JS::Value *vp) {
METHOD_HEADER(0)

bool Blob::bytes(JSContext *cx, HandleObject self, MutableHandleValue rval) {
JS::RootedObject promise(cx, JS::NewPromiseObject(cx, nullptr));
if (!promise) {
return false;
}

args.rval().setObject(*promise);
rval.setObject(*promise);

JS::RootedObject buffer(cx, data_to_owned_array_buffer(cx, self));
if (!buffer) {
Expand All @@ -317,9 +332,7 @@ bool Blob::bytes(JSContext *cx, unsigned argc, JS::Value *vp) {
return true;
}

bool Blob::slice(JSContext *cx, unsigned argc, JS::Value *vp) {
METHOD_HEADER(0)

bool Blob::slice(JSContext *cx, HandleObject self, const CallArgs &args, MutableHandleValue rval) {
auto src = Blob::blob(self);
int64_t size = src->length();
int64_t start = 0;
Expand Down Expand Up @@ -364,13 +377,11 @@ bool Blob::slice(JSContext *cx, unsigned argc, JS::Value *vp) {
return false;
}

args.rval().setObject(*new_blob);
rval.setObject(*new_blob);
return true;
}

bool Blob::stream(JSContext *cx, unsigned argc, JS::Value *vp) {
METHOD_HEADER(0)

bool Blob::stream(JSContext *cx, HandleObject self, MutableHandleValue rval) {
auto native_stream = streams::NativeStreamSource::create(cx, self, JS::UndefinedHandleValue,
stream_pull, stream_cancel);

Expand All @@ -392,19 +403,17 @@ bool Blob::stream(JSContext *cx, unsigned argc, JS::Value *vp) {
return false;
}

args.rval().setObject(*stream);
rval.setObject(*stream);
return true;
}

bool Blob::text(JSContext *cx, unsigned argc, JS::Value *vp) {
METHOD_HEADER(0)

bool Blob::text(JSContext *cx, HandleObject self, MutableHandleValue rval) {
JS::RootedObject promise(cx, JS::NewPromiseObject(cx, nullptr));
if (!promise) {
return false;
}

args.rval().setObject(*promise);
rval.setObject(*promise);

auto src = Blob::blob(self);
auto encoding = const_cast<jsencoding::Encoding *>(jsencoding::encoding_for_label_no_replacement(
Expand Down Expand Up @@ -512,6 +521,15 @@ Blob::LineEndings Blob::line_endings(JSObject *self) {
JS::GetReservedSlot(self, static_cast<size_t>(Blob::Slots::Endings)).toInt32());
}

bool Blob::is_instance(const JSObject *obj) {
return obj != nullptr &&
(JS::GetClass(obj) == &Blob::class_ || JS::GetClass(obj) == &File::class_);
}

bool Blob::is_instance(const Value val) {
return val.isObject() && is_instance(&val.toObject());
}

bool Blob::append_value(JSContext *cx, HandleObject self, HandleValue val) {
auto blob = Blob::blob(self);

Expand Down Expand Up @@ -670,17 +688,7 @@ JSObject *Blob::create(JSContext *cx, UniqueChars data, size_t data_len, HandleS
return self;
}

bool Blob::constructor(JSContext *cx, unsigned argc, JS::Value *vp) {
CTOR_HEADER("Blob", 0);

RootedValue blobParts(cx, args.get(0));
RootedValue opts(cx, args.get(1));
RootedObject self(cx, JS_NewObjectForConstructor(cx, &class_, args));

if (!self) {
return false;
}

bool Blob::init(JSContext *cx, HandleObject self, HandleValue blobParts, HandleValue opts) {
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));
Expand All @@ -699,6 +707,24 @@ bool Blob::constructor(JSContext *cx, unsigned argc, JS::Value *vp) {
return false;
}

return true;
}

bool Blob::constructor(JSContext *cx, unsigned argc, JS::Value *vp) {
CTOR_HEADER("Blob", 0);

RootedValue blobParts(cx, args.get(0));
RootedValue opts(cx, args.get(1));
RootedObject self(cx, JS_NewObjectForConstructor(cx, &class_, args));

if (!self) {
return false;
}

if (!init(cx, self, blobParts, opts)) {
return false;
}

args.rval().setObject(*self);
return true;
}
Expand Down
11 changes: 11 additions & 0 deletions builtins/web/blob.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,24 @@ class Blob : public TraceableBuiltinImpl<Blob> {
using ByteBuffer = js::Vector<uint8_t, 0, js::SystemAllocPolicy>;
using ReadersMap = JS::GCHashMap<HeapObj, BlobReader, js::StableCellHasher<HeapObj>, js::SystemAllocPolicy>;

static bool arrayBuffer(JSContext *cx, HandleObject self, MutableHandleValue rval);
static bool bytes(JSContext *cx, HandleObject self, MutableHandleValue rval);
static bool stream(JSContext *cx, HandleObject self, MutableHandleValue rval);
static bool text(JSContext *cx, HandleObject self, MutableHandleValue rval);
static bool slice(JSContext *cx, HandleObject self, const CallArgs &args, MutableHandleValue rval);

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 bool is_instance(const JSObject *obj);
static bool is_instance(const Value val);
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);
static bool init(JSContext *cx, HandleObject self, HandleValue blobParts, HandleValue opts);

static bool stream_cancel(JSContext *cx, JS::CallArgs args, JS::HandleObject stream,
JS::HandleObject owner, JS::HandleValue reason);
Expand All @@ -75,6 +85,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, UniqueChars data, size_t data_len, HandleString type);

static bool init_class(JSContext *cx, HandleObject global);
Expand Down
2 changes: 1 addition & 1 deletion builtins/web/fetch/request-response.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ bool RequestOrResponse::extract_body(JSContext *cx, JS::HandleObject self,

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

Expand Down
145 changes: 145 additions & 0 deletions builtins/web/file.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
#include "file.h"
#include "blob.h"

namespace {

bool read_last_modified(JSContext *cx, HandleValue initv, int64_t *last_modified) {
if (initv.isObject()) {
JS::RootedObject opts(cx, &initv.toObject());
JS::RootedValue val(cx);

if (!JS_GetProperty(cx, opts, "lastModified", &val)) {
return false;
}

if (!val.isUndefined()) {
return ToInt64(cx, val, last_modified);
}
}

// If the last modification date and time are not known, the attribute must return the
// current date and time representing the number of milliseconds since the Unix Epoch;
*last_modified = JS_Now() / 1000LL; // JS_Now() gives microseconds, convert it to ms.
return true;
}

} // namespace

namespace builtins {
namespace web {
namespace file {

using blob::Blob;

const JSFunctionSpec File::static_methods[] = {
JS_FS_END,
};

const JSPropertySpec File::static_properties[] = {
JS_PS_END,
};

const JSFunctionSpec File::methods[] = {
JS_FS_END,
};

const JSPropertySpec File::properties[] = {
JS_PSG("name", File::name_get, JSPROP_ENUMERATE),
JS_PSG("lastModified", File::lastModified_get, JSPROP_ENUMERATE),
JS_STRING_SYM_PS(toStringTag, "File", JSPROP_READONLY),
JS_PS_END,
};

bool File::name_get(JSContext *cx, unsigned argc, JS::Value *vp) {
METHOD_HEADER(0);
// TODO: Change this class so that its prototype isn't an instance of the class
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify what is meant by this? Is the instance prototype not supposed to be an instance of the class, or the File.prototype constructor "prototype" property?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, no idea. I've been carrying this comment from other classes without giving it much thought.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reasons I forgot, our builtin initialization system can't easily make it so that the prototype of instances isn't also an instance of the builtin. Which for some builtins it needs to, while for others it's not supposed to be. Hence, this TODO is strewn all over the place :/

if (self == proto_obj) {
return api::throw_error(cx, api::Errors::WrongReceiver, "name get", "File");
}

auto name = JS::GetReservedSlot(self, static_cast<size_t>(Slots::Name)).toString();
args.rval().setString(name);
return true;
}

bool File::lastModified_get(JSContext *cx, unsigned argc, JS::Value *vp) {
METHOD_HEADER(0);
// TODO: Change this class so that its prototype isn't an instance of the class
if (self == proto_obj) {
return api::throw_error(cx, api::Errors::WrongReceiver, "lastModified get", "File");
}

auto lastModified =
JS::GetReservedSlot(self, static_cast<size_t>(Slots::LastModified)).toNumber();
args.rval().setNumber(lastModified);
return true;
}

// https://w3c.github.io/FileAPI/#file-constructor
bool File::init(JSContext *cx, HandleObject self, HandleValue fileBits, HandleValue fileName,
HandleValue opts) {
// 1. Let bytes be the result of processing blob parts given fileBits and options.
if (!Blob::init(cx, self, fileBits, opts)) {
return false;
}

// 2. Let n be the fileName argument to the constructor.
RootedString name(cx, JS::ToString(cx, fileName));
if (!name) {
return false;
}

// 3. Process `FilePropertyBag` dictionary argument by running the following substeps:
// 1. and 2 - the steps for processing a `type` member are ensured by Blob implementation.
// 3. If the `lastModified` member is provided, let d be set to the lastModified dictionary
// member. If it is not provided, set d to the current date and time represented as the number of
// milliseconds since the Unix Epoch.
int64_t lastModified;
if (!read_last_modified(cx, opts, &lastModified)) {
return false;
}

// Return a new File object F such that:
// 2. F refers to the bytes byte sequence.
// 3. F.size is set to the number of total bytes in bytes.
// 4. F.name is set to n.
// 5. F.type is set to t.
// 6. F.lastModified is set to d.
//
// Steps 2, 3 and 5 are handled by Blob. We extend the Blob by adding a `name`
// and the `lastModified` properties.
SetReservedSlot(self, static_cast<uint32_t>(Slots::Name), JS::StringValue(name));
SetReservedSlot(self, static_cast<uint32_t>(Slots::LastModified), JS::NumberValue(lastModified));

return true;
}

bool File::constructor(JSContext *cx, unsigned argc, JS::Value *vp) {
CTOR_HEADER("File", 2);

RootedValue fileBits(cx, args.get(0));
RootedValue fileName(cx, args.get(1));
RootedValue opts(cx, args.get(2));

RootedObject self(cx, JS_NewObjectForConstructor(cx, &class_, args));
if (!self) {
return false;
}

if (!init(cx, self, fileBits, fileName, opts)) {
return false;
}

args.rval().setObject(*self);
return true;
}

bool File::init_class(JSContext *cx, JS::HandleObject global) {
return init_class_impl(cx, global, Blob::proto_obj);
}

bool install(api::Engine *engine) { return File::init_class(engine->cx(), engine->global()); }

} // namespace file
} // namespace web
} // namespace builtins
Loading
Loading