From 5cdc5384338fc292e227875ba3b459dbc38cf09b Mon Sep 17 00:00:00 2001 From: Tomasz Andrzejak Date: Wed, 4 Dec 2024 15:30:01 +0100 Subject: [PATCH 01/10] Implement File API --- builtins/web/blob.cpp | 29 +++ builtins/web/blob.h | 2 + builtins/web/file.cpp | 226 ++++++++++++++++++ builtins/web/file.h | 47 ++++ cmake/builtins.cmake | 1 + .../FileAPI/file/File-constructor.any.js.json | 149 ++++++++++++ tests/wpt-harness/pre-harness.js | 1 - tests/wpt-harness/tests.json | 1 + 8 files changed, 455 insertions(+), 1 deletion(-) create mode 100644 builtins/web/file.cpp create mode 100644 builtins/web/file.h create mode 100644 tests/wpt-harness/expectations/FileAPI/file/File-constructor.any.js.json diff --git a/builtins/web/blob.cpp b/builtins/web/blob.cpp index 360b94e0..6bd4d18f 100644 --- a/builtins/web/blob.cpp +++ b/builtins/web/blob.cpp @@ -672,6 +672,35 @@ JSObject *Blob::create(JSContext *cx, UniqueChars data, size_t data_len, HandleS return self; } +JSObject *Blob::create(JSContext *cx, HandleValue blobParts, HandleValue opts) { + RootedObject self(cx, JS_NewObjectWithGivenProto(cx, &class_, proto_obj)); + + if (!self) { + return nullptr; + } + + SetReservedSlot(self, static_cast(Slots::Type), JS_GetEmptyStringValue(cx)); + SetReservedSlot(self, static_cast(Slots::Endings), JS::Int32Value(LineEndings::Transparent)); + SetReservedSlot(self, static_cast(Slots::Data), JS::PrivateValue(new ByteBuffer)); + SetReservedSlot(self, static_cast(Slots::Readers), JS::PrivateValue(new ReadersMap)); + + if (blobParts.isNull()) { + api::throw_error(cx, api::Errors::TypeError, "Blob.constructor", "blobParts", "be an object"); + return nullptr; + } + + // Walk the blob parts and append them to the blob's buffer. + if (!blobParts.isUndefined() && !init_blob_parts(cx, self, blobParts)) { + return nullptr; + } + + if (!opts.isNullOrUndefined() && !init_options(cx, self, opts)) { + return nullptr; + } + + return self; +} + bool Blob::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { CTOR_HEADER("Blob", 0); diff --git a/builtins/web/blob.h b/builtins/web/blob.h index 8efe2d25..42b6f351 100644 --- a/builtins/web/blob.h +++ b/builtins/web/blob.h @@ -75,7 +75,9 @@ class Blob : public TraceableBuiltinImpl { 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 JSObject *create(JSContext *cx, HandleValue blobParts, HandleValue opts); static bool init_class(JSContext *cx, HandleObject global); static bool constructor(JSContext *cx, unsigned argc, Value *vp); diff --git a/builtins/web/file.cpp b/builtins/web/file.cpp new file mode 100644 index 00000000..172d3ff3 --- /dev/null +++ b/builtins/web/file.cpp @@ -0,0 +1,226 @@ +#include "file.h" +#include "blob.h" +#include "js/TypeDecls.h" +#include "mozilla/Assertions.h" + +namespace builtins { +namespace web { +namespace file { + +bool maybe_file_instance(JSContext *cx, HandleValue value, MutableHandleValue instance) { + instance.setNull(); + + JS::ForOfIterator it(cx); + if (!it.init(value, JS::ForOfIterator::AllowNonIterable)) { + return false; + } + + bool is_iterable = value.isObject() && it.valueIsIterable(); + bool done; + + if (is_iterable) { + JS::RootedValue item(cx); + + if (!it.next(&item, &done)) { + return false; + } + if (item.isObject() && File::is_instance(&item.toObject())) { + instance.setObject(item.toObject()); + } + } + + return true; +} + +using blob::Blob; + +const JSFunctionSpec File::static_methods[] = { + JS_FS_END, +}; + +const JSPropertySpec File::static_properties[] = { + JS_PS_END, +}; + +const JSFunctionSpec File::methods[] = { + JS_FN("arrayBuffer", File::arrayBuffer, 0, JSPROP_ENUMERATE), + JS_FN("bytes", File::bytes, 0, JSPROP_ENUMERATE), + JS_FN("slice", File::slice, 0, JSPROP_ENUMERATE), + JS_FN("stream", File::stream, 0, JSPROP_ENUMERATE), + JS_FN("text", File::text, 0, JSPROP_ENUMERATE), + JS_FS_END, +}; + +const JSPropertySpec File::properties[] = { + JS_PSG("size", File::size_get, JSPROP_ENUMERATE), + JS_PSG("type", File::type_get, JSPROP_ENUMERATE), + 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, +}; + +#define DEFINE_BLOB_DELEGATE(name) \ +bool File::name(JSContext *cx, unsigned argc, JS::Value *vp) { \ + METHOD_HEADER(0) \ + RootedObject blob(cx, File::blob(self)); \ + return JS::Call(cx, blob, #name, JS::HandleValueArray(args), args.rval()); \ +} + +DEFINE_BLOB_DELEGATE(arrayBuffer) +DEFINE_BLOB_DELEGATE(bytes) +DEFINE_BLOB_DELEGATE(slice) +DEFINE_BLOB_DELEGATE(stream) +DEFINE_BLOB_DELEGATE(text) + +bool File::size_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, "size get", "File"); + } + + RootedObject blob(cx, File::blob(self)); + args.rval().setNumber(Blob::blob_size(blob)); + return true; +} + +bool File::type_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, "type get", "File"); + } + + RootedObject blob(cx, File::blob(self)); + args.rval().setString(Blob::type(blob)); + return true; +} + +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 + if (self == proto_obj) { + return api::throw_error(cx, api::Errors::WrongReceiver, "name get", "File"); + } + + auto name = JS::GetReservedSlot(self, static_cast(File::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(File::Slots::LastModified)).toInt32(); + args.rval().setNumber(lastModified); + return true; +} + +bool File::init_last_modified(JSContext *cx, HandleObject self, HandleValue initv) { + bool has_last_modified = false; + + JS::RootedValue init_val(cx, initv); + JS::RootedObject opts(cx, init_val.toObjectOrNull()); + + if (!opts) { + return true; + } + + if (!JS_HasProperty(cx, opts, "lastModified", &has_last_modified)) { + return false; + } + + if (has_last_modified) { + JS::RootedValue ts(cx); + if (!JS_GetProperty(cx, opts, "lastModified", &ts)) { + return false; + } + + if (ts.isNumber()) { + SetReservedSlot(self, static_cast(Slots::LastModified), ts); + } + } + + return true; +} + +JSObject *File::blob(JSObject *self) { + MOZ_ASSERT(is_instance(self)); + auto blob = &JS::GetReservedSlot(self, static_cast(Blob::Slots::Data)).toObject(); + + MOZ_ASSERT(blob); + return blob; +} + +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; + } + + RootedValue other(cx); + if (!maybe_file_instance(cx, fileBits, &other)) { + return false; + } + + if (!other.isNull()) { + MOZ_ASSERT(other.isObject()); + RootedObject other_blob(cx, File::blob(&other.toObject())); + RootedValue blob_copy(cx); + + if (!Call(cx, other_blob, "slice", HandleValueArray::empty(), &blob_copy)) { + return false; + } + + SetReservedSlot(self, static_cast(Slots::Blob), blob_copy); + } else { + RootedObject blob(cx, Blob::create(cx, fileBits, opts)); + if (!blob) { + return false; + } + SetReservedSlot(self, static_cast(Slots::Blob), JS::ObjectValue(*blob)); + } + + + RootedString name(cx, JS::ToString(cx, fileName)); + if (!name) { + return false; + } + SetReservedSlot(self, static_cast(Slots::Name), JS::StringValue(name)); + + if (!opts.isNullOrUndefined()) { + if (!init_last_modified(cx, self, opts)) { + return false; + } + } else { + auto now = std::chrono::system_clock::now(); + auto ms_since_epoch = std::chrono::duration_cast(now.time_since_epoch()).count(); + SetReservedSlot(self, static_cast(Slots::LastModified), JS::Int32Value(ms_since_epoch)); + } + + args.rval().setObject(*self); + return true; +} + +bool File::init_class(JSContext *cx, JS::HandleObject global) { + return init_class_impl(cx, global); +} + +bool install(api::Engine *engine) { + return File::init_class(engine->cx(), engine->global()); +} + +} // namespace file +} // namespace web +} // namespace builtins diff --git a/builtins/web/file.h b/builtins/web/file.h new file mode 100644 index 00000000..dd684876 --- /dev/null +++ b/builtins/web/file.h @@ -0,0 +1,47 @@ +#ifndef BUILTINS_WEB_FILE_H +#define BUILTINS_WEB_FILE_H + +#include "builtin.h" + +namespace builtins { +namespace web { +namespace file { + +class File: public BuiltinImpl { + static bool arrayBuffer(JSContext *cx, unsigned argc, JS::Value *vp); + static bool bytes(JSContext *cx, unsigned argc, JS::Value *vp); + static bool slice(JSContext *cx, unsigned argc, JS::Value *vp); + static bool stream(JSContext *cx, unsigned argc, JS::Value *vp); + static bool text(JSContext *cx, unsigned argc, JS::Value *vp); + + static bool size_get(JSContext *cx, unsigned argc, JS::Value *vp); + static bool type_get(JSContext *cx, unsigned argc, JS::Value *vp); + static bool name_get(JSContext *cx, unsigned argc, JS::Value *vp); + static bool lastModified_get(JSContext *cx, unsigned argc, JS::Value *vp); + + static bool init_last_modified(JSContext *cx, HandleObject self, HandleValue initv); + +public: + enum Slots { Blob, Name, LastModified, Count }; + + static constexpr const char *class_name = "File"; + static constexpr unsigned ctor_length = 2; + + static const JSFunctionSpec static_methods[]; + static const JSPropertySpec static_properties[]; + static const JSFunctionSpec methods[]; + static const JSPropertySpec properties[]; + + static JSObject *blob(JSObject *self); + + static bool init_class(JSContext *cx, HandleObject global); + static bool constructor(JSContext *cx, unsigned argc, Value *vp); +}; + +bool install(api::Engine *engine); + +} // namespace file +} // namespace web +} // namespace builtins + +#endif // BUILTINS_WEB_FILE_H diff --git a/cmake/builtins.cmake b/cmake/builtins.cmake index 16cc09f1..474d4ffc 100644 --- a/cmake/builtins.cmake +++ b/cmake/builtins.cmake @@ -18,6 +18,7 @@ add_builtin(builtins/web/queue-microtask.cpp) add_builtin(builtins/web/structured-clone.cpp) add_builtin(builtins/web/base64.cpp) add_builtin(builtins/web/blob.cpp) +add_builtin(builtins/web/file.cpp) add_builtin( builtins::web::dom_exception SRC diff --git a/tests/wpt-harness/expectations/FileAPI/file/File-constructor.any.js.json b/tests/wpt-harness/expectations/FileAPI/file/File-constructor.any.js.json new file mode 100644 index 00000000..d0f41a63 --- /dev/null +++ b/tests/wpt-harness/expectations/FileAPI/file/File-constructor.any.js.json @@ -0,0 +1,149 @@ +{ + "File interface object exists": { + "status": "PASS" + }, + "Required arguments": { + "status": "PASS" + }, + "empty fileBits": { + "status": "PASS" + }, + "DOMString fileBits": { + "status": "PASS" + }, + "Unicode DOMString fileBits": { + "status": "PASS" + }, + "String object fileBits": { + "status": "PASS" + }, + "Empty Blob fileBits": { + "status": "PASS" + }, + "Blob fileBits": { + "status": "PASS" + }, + "Empty File fileBits": { + "status": "PASS" + }, + "File fileBits": { + "status": "PASS" + }, + "ArrayBuffer fileBits": { + "status": "PASS" + }, + "Typed array fileBits": { + "status": "PASS" + }, + "Various fileBits": { + "status": "PASS" + }, + "Number in fileBits": { + "status": "PASS" + }, + "Array in fileBits": { + "status": "PASS" + }, + "Object in fileBits": { + "status": "PASS" + }, + "Object with toString in fileBits": { + "status": "PASS" + }, + "Custom @@iterator": { + "status": "PASS" + }, + "Invalid bits argument: \"hello\"": { + "status": "PASS" + }, + "Invalid bits argument: 0": { + "status": "PASS" + }, + "Invalid bits argument: null": { + "status": "PASS" + }, + "Bits argument: object that throws": { + "status": "PASS" + }, + "Using fileName": { + "status": "PASS" + }, + "No replacement when using special character in fileName": { + "status": "PASS" + }, + "Using null fileName": { + "status": "PASS" + }, + "Using number fileName": { + "status": "PASS" + }, + "Using empty string fileName": { + "status": "PASS" + }, + "Using type in File constructor: text/plain": { + "status": "PASS" + }, + "Using type in File constructor: text/plain;charset=UTF-8": { + "status": "PASS" + }, + "Using type in File constructor: TEXT/PLAIN": { + "status": "PASS" + }, + "Using type in File constructor: 𝓽𝓮𝔁𝓽/𝔭𝔩𝔞𝔦𝔫": { + "status": "PASS" + }, + "Using type in File constructor: ascii/nonprintable\u001f": { + "status": "PASS" + }, + "Using type in File constructor: ascii/nonprintable": { + "status": "PASS" + }, + "Using type in File constructor: nonasciiî": { + "status": "PASS" + }, + "Using type in File constructor: nonasciiሴ": { + "status": "PASS" + }, + "Using type in File constructor: nonparsable": { + "status": "PASS" + }, + "Using lastModified": { + "status": "PASS" + }, + "Misusing name": { + "status": "PASS" + }, + "Unknown properties are ignored": { + "status": "PASS" + }, + "Invalid property bag: 123": { + "status": "PASS" + }, + "Invalid property bag: 123.4": { + "status": "PASS" + }, + "Invalid property bag: true": { + "status": "PASS" + }, + "Invalid property bag: \"abc\"": { + "status": "PASS" + }, + "Unusual but valid property bag: null": { + "status": "PASS" + }, + "Unusual but valid property bag: undefined": { + "status": "PASS" + }, + "Unusual but valid property bag: 1,2,3": { + "status": "PASS" + }, + "Unusual but valid property bag: /regex/": { + "status": "PASS" + }, + "Unusual but valid property bag: function() {}": { + "status": "PASS" + }, + "Property bag propagates exceptions": { + "status": "PASS" + } +} diff --git a/tests/wpt-harness/pre-harness.js b/tests/wpt-harness/pre-harness.js index d4d2991e..1b198a15 100644 --- a/tests/wpt-harness/pre-harness.js +++ b/tests/wpt-harness/pre-harness.js @@ -31,5 +31,4 @@ globalThis.crypto.subtle.generateKey = function () {return Promise.reject(new Er globalThis.FormData = class FormData{}; globalThis.SharedArrayBuffer = class SharedArrayBuffer{}; globalThis.MessageChannel = class MessageChannel{}; -globalThis.File = class File{}; ; diff --git a/tests/wpt-harness/tests.json b/tests/wpt-harness/tests.json index 7be26535..333fa950 100644 --- a/tests/wpt-harness/tests.json +++ b/tests/wpt-harness/tests.json @@ -172,6 +172,7 @@ "FileAPI/blob/Blob-slice-overflow.any.js", "FileAPI/blob/Blob-stream.any.js", "FileAPI/blob/Blob-text.any.js", + "FileAPI/file/File-constructor.any.js", "hr-time/basic.any.js", "hr-time/idlharness.any.js", "hr-time/monotonic-clock.any.js", From e7be7c3e064a734a1761fbef091e18c7404dfdc9 Mon Sep 17 00:00:00 2001 From: Tomasz Andrzejak Date: Mon, 9 Dec 2024 17:52:45 +0100 Subject: [PATCH 02/10] Add public blob API --- builtins/web/blob.cpp | 46 +++++++++++++++---------- builtins/web/blob.h | 6 ++++ builtins/web/fetch/request-response.cpp | 2 +- builtins/web/file.cpp | 20 +++++++---- 4 files changed, 47 insertions(+), 27 deletions(-) diff --git a/builtins/web/blob.cpp b/builtins/web/blob.cpp index 6bd4d18f..87d57036 100644 --- a/builtins/web/blob.cpp +++ b/builtins/web/blob.cpp @@ -153,6 +153,18 @@ namespace blob { using js::Vector; +#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, }; @@ -269,15 +281,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) +bool Blob::arrayBuffer(JSContext *cx, HandleObject self, MutableHandleValue res) { JS::RootedObject promise(cx, JS::NewPromiseObject(cx, nullptr)); if (!promise) { return false; } - args.rval().setObject(*promise); + res.setObject(*promise); auto buffer = data_to_owned_array_buffer(cx, self); if (!buffer) { @@ -291,15 +307,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 res) { JS::RootedObject promise(cx, JS::NewPromiseObject(cx, nullptr)); if (!promise) { return false; } - args.rval().setObject(*promise); + res.setObject(*promise); JS::RootedObject buffer(cx, data_to_owned_array_buffer(cx, self)); if (!buffer) { @@ -319,9 +333,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 res) { auto src = Blob::blob(self); int64_t size = src->length(); int64_t start = 0; @@ -366,13 +378,11 @@ bool Blob::slice(JSContext *cx, unsigned argc, JS::Value *vp) { return false; } - args.rval().setObject(*new_blob); + res.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 res) { auto native_stream = streams::NativeStreamSource::create(cx, self, JS::UndefinedHandleValue, stream_pull, stream_cancel); @@ -394,19 +404,17 @@ bool Blob::stream(JSContext *cx, unsigned argc, JS::Value *vp) { return false; } - args.rval().setObject(*stream); + res.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 res) { JS::RootedObject promise(cx, JS::NewPromiseObject(cx, nullptr)); if (!promise) { return false; } - args.rval().setObject(*promise); + res.setObject(*promise); auto src = Blob::blob(self); auto encoding = const_cast(jsencoding::encoding_for_label_no_replacement( diff --git a/builtins/web/blob.h b/builtins/web/blob.h index 42b6f351..80541e0e 100644 --- a/builtins/web/blob.h +++ b/builtins/web/blob.h @@ -58,6 +58,12 @@ class Blob : public TraceableBuiltinImpl { using ByteBuffer = js::Vector; using ReadersMap = JS::GCHashMap, js::SystemAllocPolicy>; + static bool arrayBuffer(JSContext *cx, HandleObject self, MutableHandleValue result); + static bool bytes(JSContext *cx, HandleObject self, MutableHandleValue result); + static bool stream(JSContext *cx, HandleObject self, MutableHandleValue result); + static bool text(JSContext *cx, HandleObject self, MutableHandleValue result); + static bool slice(JSContext *cx, HandleObject self, const CallArgs &args, MutableHandleValue result); + static ReadersMap *readers(JSObject *self); static ByteBuffer *blob(JSObject *self); static size_t blob_size(JSObject *self); diff --git a/builtins/web/fetch/request-response.cpp b/builtins/web/fetch/request-response.cpp index 855b9aa5..9a4ccc1a 100644 --- a/builtins/web/fetch/request-response.cpp +++ b/builtins/web/fetch/request-response.cpp @@ -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; } diff --git a/builtins/web/file.cpp b/builtins/web/file.cpp index 172d3ff3..f632c0ca 100644 --- a/builtins/web/file.cpp +++ b/builtins/web/file.cpp @@ -1,5 +1,6 @@ #include "file.h" #include "blob.h" +#include "js/CallArgs.h" #include "js/TypeDecls.h" #include "mozilla/Assertions.h" @@ -60,19 +61,24 @@ const JSPropertySpec File::properties[] = { JS_PS_END, }; -#define DEFINE_BLOB_DELEGATE(name) \ -bool File::name(JSContext *cx, unsigned argc, JS::Value *vp) { \ - METHOD_HEADER(0) \ - RootedObject blob(cx, File::blob(self)); \ - return JS::Call(cx, blob, #name, JS::HandleValueArray(args), args.rval()); \ +#define DEFINE_BLOB_DELEGATE(name) \ +bool File::name(JSContext *cx, unsigned argc, JS::Value *vp) { \ + METHOD_HEADER(0) \ + RootedObject blob(cx, File::blob(self)); \ + return Blob::name(cx, blob, args.rval()); \ } DEFINE_BLOB_DELEGATE(arrayBuffer) DEFINE_BLOB_DELEGATE(bytes) -DEFINE_BLOB_DELEGATE(slice) DEFINE_BLOB_DELEGATE(stream) DEFINE_BLOB_DELEGATE(text) +bool File::slice(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(0) + RootedObject blob(cx, File::blob(self)); + return Blob::slice(cx, blob, args, args.rval()); +} + bool File::size_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 @@ -179,7 +185,7 @@ bool File::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { RootedObject other_blob(cx, File::blob(&other.toObject())); RootedValue blob_copy(cx); - if (!Call(cx, other_blob, "slice", HandleValueArray::empty(), &blob_copy)) { + if (!Blob::slice(cx, other_blob, CallArgs(), &blob_copy)) { return false; } From 58e2d05f494dc702c46937f99b15acb344d248d6 Mon Sep 17 00:00:00 2001 From: Tomasz Andrzejak Date: Mon, 9 Dec 2024 18:01:49 +0100 Subject: [PATCH 03/10] Add comment about copying blob content --- builtins/web/file.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/builtins/web/file.cpp b/builtins/web/file.cpp index f632c0ca..6af4836d 100644 --- a/builtins/web/file.cpp +++ b/builtins/web/file.cpp @@ -185,6 +185,7 @@ bool File::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { RootedObject other_blob(cx, File::blob(&other.toObject())); RootedValue blob_copy(cx); + // Calling slice with no arguments copies the entire blob's content. if (!Blob::slice(cx, other_blob, CallArgs(), &blob_copy)) { return false; } From 923eb3d8dad2cf0260909c8c6d861be568832149 Mon Sep 17 00:00:00 2001 From: Tomasz Andrzejak Date: Mon, 9 Dec 2024 19:28:19 +0100 Subject: [PATCH 04/10] Rename res to rval --- builtins/web/blob.cpp | 20 ++++++++++---------- builtins/web/blob.h | 10 +++++----- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/builtins/web/blob.cpp b/builtins/web/blob.cpp index 87d57036..2b33c2fb 100644 --- a/builtins/web/blob.cpp +++ b/builtins/web/blob.cpp @@ -287,13 +287,13 @@ DEFINE_BLOB_METHOD(stream) DEFINE_BLOB_METHOD(text) DEFINE_BLOB_METHOD_W_ARGS(slice) -bool Blob::arrayBuffer(JSContext *cx, HandleObject self, MutableHandleValue res) { +bool Blob::arrayBuffer(JSContext *cx, HandleObject self, MutableHandleValue rval) { JS::RootedObject promise(cx, JS::NewPromiseObject(cx, nullptr)); if (!promise) { return false; } - res.setObject(*promise); + rval.setObject(*promise); auto buffer = data_to_owned_array_buffer(cx, self); if (!buffer) { @@ -307,13 +307,13 @@ bool Blob::arrayBuffer(JSContext *cx, HandleObject self, MutableHandleValue res) return true; } -bool Blob::bytes(JSContext *cx, HandleObject self, MutableHandleValue res) { +bool Blob::bytes(JSContext *cx, HandleObject self, MutableHandleValue rval) { JS::RootedObject promise(cx, JS::NewPromiseObject(cx, nullptr)); if (!promise) { return false; } - res.setObject(*promise); + rval.setObject(*promise); JS::RootedObject buffer(cx, data_to_owned_array_buffer(cx, self)); if (!buffer) { @@ -333,7 +333,7 @@ bool Blob::bytes(JSContext *cx, HandleObject self, MutableHandleValue res) { return true; } -bool Blob::slice(JSContext *cx, HandleObject self, const CallArgs &args, MutableHandleValue res) { +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; @@ -378,11 +378,11 @@ bool Blob::slice(JSContext *cx, HandleObject self, const CallArgs &args, Mutable return false; } - res.setObject(*new_blob); + rval.setObject(*new_blob); return true; } -bool Blob::stream(JSContext *cx, HandleObject self, MutableHandleValue res) { +bool Blob::stream(JSContext *cx, HandleObject self, MutableHandleValue rval) { auto native_stream = streams::NativeStreamSource::create(cx, self, JS::UndefinedHandleValue, stream_pull, stream_cancel); @@ -404,17 +404,17 @@ bool Blob::stream(JSContext *cx, HandleObject self, MutableHandleValue res) { return false; } - res.setObject(*stream); + rval.setObject(*stream); return true; } -bool Blob::text(JSContext *cx, HandleObject self, MutableHandleValue res) { +bool Blob::text(JSContext *cx, HandleObject self, MutableHandleValue rval) { JS::RootedObject promise(cx, JS::NewPromiseObject(cx, nullptr)); if (!promise) { return false; } - res.setObject(*promise); + rval.setObject(*promise); auto src = Blob::blob(self); auto encoding = const_cast(jsencoding::encoding_for_label_no_replacement( diff --git a/builtins/web/blob.h b/builtins/web/blob.h index 80541e0e..3bf45f6b 100644 --- a/builtins/web/blob.h +++ b/builtins/web/blob.h @@ -58,11 +58,11 @@ class Blob : public TraceableBuiltinImpl { using ByteBuffer = js::Vector; using ReadersMap = JS::GCHashMap, js::SystemAllocPolicy>; - static bool arrayBuffer(JSContext *cx, HandleObject self, MutableHandleValue result); - static bool bytes(JSContext *cx, HandleObject self, MutableHandleValue result); - static bool stream(JSContext *cx, HandleObject self, MutableHandleValue result); - static bool text(JSContext *cx, HandleObject self, MutableHandleValue result); - static bool slice(JSContext *cx, HandleObject self, const CallArgs &args, MutableHandleValue result); + 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); From 537903d64b3074b7af4998b34db97166d0cbd889 Mon Sep 17 00:00:00 2001 From: Tomasz Andrzejak Date: Thu, 12 Dec 2024 17:52:16 +0100 Subject: [PATCH 05/10] Refactor File builtin --- builtins/web/blob.h | 2 +- builtins/web/file.cpp | 224 ++++++++++++++++++------------------------ builtins/web/file.h | 16 +-- 3 files changed, 99 insertions(+), 143 deletions(-) diff --git a/builtins/web/blob.h b/builtins/web/blob.h index 3bf45f6b..cda0b1e8 100644 --- a/builtins/web/blob.h +++ b/builtins/web/blob.h @@ -51,7 +51,7 @@ class Blob : public TraceableBuiltinImpl { static const JSPropertySpec properties[]; static constexpr unsigned ctor_length = 0; - enum Slots { Data, Type, Endings, Readers, Count }; + enum Slots { Data, Type, Endings, Readers, Reserved2, Reserved1, Count }; enum LineEndings { Transparent, Native }; using HeapObj = Heap; diff --git a/builtins/web/file.cpp b/builtins/web/file.cpp index 6af4836d..cbcb5054 100644 --- a/builtins/web/file.cpp +++ b/builtins/web/file.cpp @@ -1,40 +1,62 @@ #include "file.h" #include "blob.h" + +#include "js/CallAndConstruct.h" #include "js/CallArgs.h" #include "js/TypeDecls.h" -#include "mozilla/Assertions.h" -namespace builtins { -namespace web { -namespace file { - -bool maybe_file_instance(JSContext *cx, HandleValue value, MutableHandleValue instance) { - instance.setNull(); - - JS::ForOfIterator it(cx); - if (!it.init(value, JS::ForOfIterator::AllowNonIterable)) { - return false; - } +#include "mozilla/Assertions.h" - bool is_iterable = value.isObject() && it.valueIsIterable(); - bool done; +namespace { - if (is_iterable) { - JS::RootedValue item(cx); +bool init_last_modified(JSContext *cx, HandleValue initv, MutableHandleValue rval) { + JS::RootedValue init_val(cx, initv); - if (!it.next(&item, &done)) { - return false; - } - if (item.isObject() && File::is_instance(&item.toObject())) { - instance.setObject(item.toObject()); + if (!init_val.isNullOrUndefined()) { + JS::RootedObject opts(cx, init_val.toObjectOrNull()); + + if (opts) { + bool has_last_modified = false; + if (!JS_HasProperty(cx, opts, "lastModified", &has_last_modified)) { + return false; + } + + if (has_last_modified) { + JS::RootedValue ts(cx); + if (!JS_GetProperty(cx, opts, "lastModified", &ts)) { + return false; + } + + if (ts.isNumber()) { + rval.set(ts); + return true; + } + } } } + // If `lastModified` is not provided, set d to the current date and time. + auto now = std::chrono::system_clock::now(); + auto ms_since_epoch = std::chrono::duration_cast(now.time_since_epoch()).count(); + + rval.setInt32(ms_since_epoch); return true; } +} // namespace + +namespace builtins { +namespace web { +namespace file { + + using blob::Blob; +enum ParentSlots { + Name = Blob::Slots::Reserved1, + LastModified = Blob::Slots::Reserved2, +}; + const JSFunctionSpec File::static_methods[] = { JS_FS_END, }; @@ -44,65 +66,16 @@ const JSPropertySpec File::static_properties[] = { }; const JSFunctionSpec File::methods[] = { - JS_FN("arrayBuffer", File::arrayBuffer, 0, JSPROP_ENUMERATE), - JS_FN("bytes", File::bytes, 0, JSPROP_ENUMERATE), - JS_FN("slice", File::slice, 0, JSPROP_ENUMERATE), - JS_FN("stream", File::stream, 0, JSPROP_ENUMERATE), - JS_FN("text", File::text, 0, JSPROP_ENUMERATE), JS_FS_END, }; const JSPropertySpec File::properties[] = { - JS_PSG("size", File::size_get, JSPROP_ENUMERATE), - JS_PSG("type", File::type_get, JSPROP_ENUMERATE), 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, }; -#define DEFINE_BLOB_DELEGATE(name) \ -bool File::name(JSContext *cx, unsigned argc, JS::Value *vp) { \ - METHOD_HEADER(0) \ - RootedObject blob(cx, File::blob(self)); \ - return Blob::name(cx, blob, args.rval()); \ -} - -DEFINE_BLOB_DELEGATE(arrayBuffer) -DEFINE_BLOB_DELEGATE(bytes) -DEFINE_BLOB_DELEGATE(stream) -DEFINE_BLOB_DELEGATE(text) - -bool File::slice(JSContext *cx, unsigned argc, JS::Value *vp) { - METHOD_HEADER(0) - RootedObject blob(cx, File::blob(self)); - return Blob::slice(cx, blob, args, args.rval()); -} - -bool File::size_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, "size get", "File"); - } - - RootedObject blob(cx, File::blob(self)); - args.rval().setNumber(Blob::blob_size(blob)); - return true; -} - -bool File::type_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, "type get", "File"); - } - - RootedObject blob(cx, File::blob(self)); - args.rval().setString(Blob::type(blob)); - return true; -} - 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 @@ -110,7 +83,7 @@ bool File::name_get(JSContext *cx, unsigned argc, JS::Value *vp) { return api::throw_error(cx, api::Errors::WrongReceiver, "name get", "File"); } - auto name = JS::GetReservedSlot(self, static_cast(File::Slots::Name)).toString(); + auto name = JS::GetReservedSlot(self, static_cast(ParentSlots::Name)).toString(); args.rval().setString(name); return true; } @@ -122,39 +95,11 @@ bool File::lastModified_get(JSContext *cx, unsigned argc, JS::Value *vp) { return api::throw_error(cx, api::Errors::WrongReceiver, "lastModified get", "File"); } - auto lastModified = JS::GetReservedSlot(self, static_cast(File::Slots::LastModified)).toInt32(); + auto lastModified = JS::GetReservedSlot(self, static_cast(ParentSlots::LastModified)).toInt32(); args.rval().setNumber(lastModified); return true; } -bool File::init_last_modified(JSContext *cx, HandleObject self, HandleValue initv) { - bool has_last_modified = false; - - JS::RootedValue init_val(cx, initv); - JS::RootedObject opts(cx, init_val.toObjectOrNull()); - - if (!opts) { - return true; - } - - if (!JS_HasProperty(cx, opts, "lastModified", &has_last_modified)) { - return false; - } - - if (has_last_modified) { - JS::RootedValue ts(cx); - if (!JS_GetProperty(cx, opts, "lastModified", &ts)) { - return false; - } - - if (ts.isNumber()) { - SetReservedSlot(self, static_cast(Slots::LastModified), ts); - } - } - - return true; -} - JSObject *File::blob(JSObject *self) { MOZ_ASSERT(is_instance(self)); auto blob = &JS::GetReservedSlot(self, static_cast(Blob::Slots::Data)).toObject(); @@ -163,6 +108,15 @@ JSObject *File::blob(JSObject *self) { return blob; } +bool File::is_instance(const JSObject *obj) { + return obj != nullptr + && (JS::GetClass(obj) == &class_ || JS::GetClass(obj) == &Blob::class_); +} + +bool File::is_instance(const Value val) { + return val.isObject() && is_instance(&val.toObject()); +} + bool File::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { CTOR_HEADER("File", 2); @@ -170,58 +124,68 @@ bool File::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { RootedValue fileName(cx, args.get(1)); RootedValue opts(cx, args.get(2)); - RootedObject self(cx, JS_NewObjectForConstructor(cx, &class_, args)); - if (!self) { + RootedObject blob_ctor(cx, JS_GetConstructor(cx, Blob::proto_obj)); + if (!blob_ctor) { return false; } - RootedValue other(cx); - if (!maybe_file_instance(cx, fileBits, &other)) { + RootedObject this_ctor(cx, JS_GetConstructor(cx, File::proto_obj)); + if (!this_ctor) { return false; } - if (!other.isNull()) { - MOZ_ASSERT(other.isObject()); - RootedObject other_blob(cx, File::blob(&other.toObject())); - RootedValue blob_copy(cx); + MOZ_ASSERT(JS::IsConstructor(blob_ctor)); + MOZ_ASSERT(JS::IsConstructor(this_ctor)); - // Calling slice with no arguments copies the entire blob's content. - if (!Blob::slice(cx, other_blob, CallArgs(), &blob_copy)) { - return false; - } + JS::RootedValueArray<2> blob_args(cx); + blob_args[0].set(fileBits); + blob_args[1].set(opts); - SetReservedSlot(self, static_cast(Slots::Blob), blob_copy); - } else { - RootedObject blob(cx, Blob::create(cx, fileBits, opts)); - if (!blob) { - return false; - } - SetReservedSlot(self, static_cast(Slots::Blob), JS::ObjectValue(*blob)); - } + // 1. Let bytes be the result of processing blob parts given fileBits and options. + // + // We call the Blob constructor on `self` object to initialize it as a Blob. + // We pass `fileBits` and `options` to Blob constructor. + RootedValue blob_ctor_val(cx, JS::ObjectValue(*blob_ctor)); + RootedObject self(cx); + if (!JS::Construct(cx, blob_ctor_val, this_ctor, blob_args, &self)) { + return false; + } + // 2. Let n be the fileName argument to the constructor. RootedString name(cx, JS::ToString(cx, fileName)); if (!name) { return false; } - SetReservedSlot(self, static_cast(Slots::Name), JS::StringValue(name)); - if (!opts.isNullOrUndefined()) { - if (!init_last_modified(cx, self, opts)) { - return false; - } - } else { - auto now = std::chrono::system_clock::now(); - auto ms_since_epoch = std::chrono::duration_cast(now.time_since_epoch()).count(); - SetReservedSlot(self, static_cast(Slots::LastModified), JS::Int32Value(ms_since_epoch)); + // 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. + RootedValue lastModified(cx); + if (!init_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(ParentSlots::Name), JS::StringValue(name)); + SetReservedSlot(self, static_cast(ParentSlots::LastModified), lastModified); + args.rval().setObject(*self); return true; } bool File::init_class(JSContext *cx, JS::HandleObject global) { - return init_class_impl(cx, global); + return init_class_impl(cx, global, Blob::proto_obj); } bool install(api::Engine *engine) { diff --git a/builtins/web/file.h b/builtins/web/file.h index dd684876..309265b5 100644 --- a/builtins/web/file.h +++ b/builtins/web/file.h @@ -6,23 +6,12 @@ namespace builtins { namespace web { namespace file { - class File: public BuiltinImpl { - static bool arrayBuffer(JSContext *cx, unsigned argc, JS::Value *vp); - static bool bytes(JSContext *cx, unsigned argc, JS::Value *vp); - static bool slice(JSContext *cx, unsigned argc, JS::Value *vp); - static bool stream(JSContext *cx, unsigned argc, JS::Value *vp); - static bool text(JSContext *cx, unsigned argc, JS::Value *vp); - - static bool size_get(JSContext *cx, unsigned argc, JS::Value *vp); - static bool type_get(JSContext *cx, unsigned argc, JS::Value *vp); static bool name_get(JSContext *cx, unsigned argc, JS::Value *vp); static bool lastModified_get(JSContext *cx, unsigned argc, JS::Value *vp); - static bool init_last_modified(JSContext *cx, HandleObject self, HandleValue initv); - public: - enum Slots { Blob, Name, LastModified, Count }; + enum Slots { Count }; static constexpr const char *class_name = "File"; static constexpr unsigned ctor_length = 2; @@ -32,6 +21,9 @@ class File: public BuiltinImpl { static const JSFunctionSpec methods[]; static const JSPropertySpec properties[]; + static bool is_instance(const JSObject *obj); + static bool is_instance(const Value val); + static JSObject *blob(JSObject *self); static bool init_class(JSContext *cx, HandleObject global); From e1d7095b36b766b0cf99a15bc2626cbcc50eb234 Mon Sep 17 00:00:00 2001 From: Tomasz Andrzejak Date: Thu, 12 Dec 2024 17:54:02 +0100 Subject: [PATCH 06/10] Remove unused function --- builtins/web/file.cpp | 8 -------- builtins/web/file.h | 2 -- 2 files changed, 10 deletions(-) diff --git a/builtins/web/file.cpp b/builtins/web/file.cpp index cbcb5054..ff82ae50 100644 --- a/builtins/web/file.cpp +++ b/builtins/web/file.cpp @@ -100,14 +100,6 @@ bool File::lastModified_get(JSContext *cx, unsigned argc, JS::Value *vp) { return true; } -JSObject *File::blob(JSObject *self) { - MOZ_ASSERT(is_instance(self)); - auto blob = &JS::GetReservedSlot(self, static_cast(Blob::Slots::Data)).toObject(); - - MOZ_ASSERT(blob); - return blob; -} - bool File::is_instance(const JSObject *obj) { return obj != nullptr && (JS::GetClass(obj) == &class_ || JS::GetClass(obj) == &Blob::class_); diff --git a/builtins/web/file.h b/builtins/web/file.h index 309265b5..95fc860c 100644 --- a/builtins/web/file.h +++ b/builtins/web/file.h @@ -24,8 +24,6 @@ class File: public BuiltinImpl { static bool is_instance(const JSObject *obj); static bool is_instance(const Value val); - static JSObject *blob(JSObject *self); - static bool init_class(JSContext *cx, HandleObject global); static bool constructor(JSContext *cx, unsigned argc, Value *vp); }; From 89f9350f14395881700e4320bc580ce905feba1e Mon Sep 17 00:00:00 2001 From: Tomasz Andrzejak Date: Thu, 12 Dec 2024 17:57:14 +0100 Subject: [PATCH 07/10] Remove unused Blob::create function --- builtins/web/blob.cpp | 29 ----------------------------- builtins/web/blob.h | 1 - 2 files changed, 30 deletions(-) diff --git a/builtins/web/blob.cpp b/builtins/web/blob.cpp index 2b33c2fb..c760b4ab 100644 --- a/builtins/web/blob.cpp +++ b/builtins/web/blob.cpp @@ -680,35 +680,6 @@ JSObject *Blob::create(JSContext *cx, UniqueChars data, size_t data_len, HandleS return self; } -JSObject *Blob::create(JSContext *cx, HandleValue blobParts, HandleValue opts) { - RootedObject self(cx, JS_NewObjectWithGivenProto(cx, &class_, proto_obj)); - - if (!self) { - return nullptr; - } - - SetReservedSlot(self, static_cast(Slots::Type), JS_GetEmptyStringValue(cx)); - SetReservedSlot(self, static_cast(Slots::Endings), JS::Int32Value(LineEndings::Transparent)); - SetReservedSlot(self, static_cast(Slots::Data), JS::PrivateValue(new ByteBuffer)); - SetReservedSlot(self, static_cast(Slots::Readers), JS::PrivateValue(new ReadersMap)); - - if (blobParts.isNull()) { - api::throw_error(cx, api::Errors::TypeError, "Blob.constructor", "blobParts", "be an object"); - return nullptr; - } - - // Walk the blob parts and append them to the blob's buffer. - if (!blobParts.isUndefined() && !init_blob_parts(cx, self, blobParts)) { - return nullptr; - } - - if (!opts.isNullOrUndefined() && !init_options(cx, self, opts)) { - return nullptr; - } - - return self; -} - bool Blob::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { CTOR_HEADER("Blob", 0); diff --git a/builtins/web/blob.h b/builtins/web/blob.h index cda0b1e8..857b789a 100644 --- a/builtins/web/blob.h +++ b/builtins/web/blob.h @@ -83,7 +83,6 @@ class Blob : public TraceableBuiltinImpl { size_t size, size_t *bytes_read); static JSObject *create(JSContext *cx, UniqueChars data, size_t data_len, HandleString type); - static JSObject *create(JSContext *cx, HandleValue blobParts, HandleValue opts); static bool init_class(JSContext *cx, HandleObject global); static bool constructor(JSContext *cx, unsigned argc, Value *vp); From 9b26b513bf444f2241d6fc7275538f25e18267fd Mon Sep 17 00:00:00 2001 From: Tomasz Andrzejak Date: Thu, 12 Dec 2024 18:14:44 +0100 Subject: [PATCH 08/10] Run clang-format on file.h and file.cpp --- builtins/web/file.cpp | 29 ++++++++++++----------------- builtins/web/file.h | 2 +- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/builtins/web/file.cpp b/builtins/web/file.cpp index ff82ae50..435c085b 100644 --- a/builtins/web/file.cpp +++ b/builtins/web/file.cpp @@ -37,24 +37,24 @@ bool init_last_modified(JSContext *cx, HandleValue initv, MutableHandleValue rva // If `lastModified` is not provided, set d to the current date and time. auto now = std::chrono::system_clock::now(); - auto ms_since_epoch = std::chrono::duration_cast(now.time_since_epoch()).count(); + auto ms_since_epoch = + std::chrono::duration_cast(now.time_since_epoch()).count(); rval.setInt32(ms_since_epoch); return true; } -} // namespace +} // namespace namespace builtins { namespace web { namespace file { - using blob::Blob; enum ParentSlots { - Name = Blob::Slots::Reserved1, - LastModified = Blob::Slots::Reserved2, + Name = Blob::Slots::Reserved1, + LastModified = Blob::Slots::Reserved2, }; const JSFunctionSpec File::static_methods[] = { @@ -95,19 +95,17 @@ bool File::lastModified_get(JSContext *cx, unsigned argc, JS::Value *vp) { return api::throw_error(cx, api::Errors::WrongReceiver, "lastModified get", "File"); } - auto lastModified = JS::GetReservedSlot(self, static_cast(ParentSlots::LastModified)).toInt32(); + auto lastModified = + JS::GetReservedSlot(self, static_cast(ParentSlots::LastModified)).toInt32(); args.rval().setNumber(lastModified); return true; } bool File::is_instance(const JSObject *obj) { - return obj != nullptr - && (JS::GetClass(obj) == &class_ || JS::GetClass(obj) == &Blob::class_); + return obj != nullptr && (JS::GetClass(obj) == &class_ || JS::GetClass(obj) == &Blob::class_); } -bool File::is_instance(const Value val) { - return val.isObject() && is_instance(&val.toObject()); -} +bool File::is_instance(const Value val) { return val.isObject() && is_instance(&val.toObject()); } bool File::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { CTOR_HEADER("File", 2); @@ -133,7 +131,6 @@ bool File::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { blob_args[0].set(fileBits); blob_args[1].set(opts); - // 1. Let bytes be the result of processing blob parts given fileBits and options. // // We call the Blob constructor on `self` object to initialize it as a Blob. @@ -152,8 +149,8 @@ bool File::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { // 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 + // 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. RootedValue lastModified(cx); if (!init_last_modified(cx, opts, &lastModified)) { @@ -180,9 +177,7 @@ 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()); -} +bool install(api::Engine *engine) { return File::init_class(engine->cx(), engine->global()); } } // namespace file } // namespace web diff --git a/builtins/web/file.h b/builtins/web/file.h index 95fc860c..a3aaf08a 100644 --- a/builtins/web/file.h +++ b/builtins/web/file.h @@ -6,7 +6,7 @@ namespace builtins { namespace web { namespace file { -class File: public BuiltinImpl { +class File : public BuiltinImpl { static bool name_get(JSContext *cx, unsigned argc, JS::Value *vp); static bool lastModified_get(JSContext *cx, unsigned argc, JS::Value *vp); From eeab147fc2959fe855ccefeec7acdc252aad4678 Mon Sep 17 00:00:00 2001 From: Tomasz Andrzejak Date: Mon, 23 Dec 2024 21:12:33 +0100 Subject: [PATCH 09/10] Add File::create function --- builtins/web/blob.cpp | 4 ++++ builtins/web/blob.h | 2 +- builtins/web/file.cpp | 43 +++++++++++++++++++++++++++++-------------- builtins/web/file.h | 1 + 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/builtins/web/blob.cpp b/builtins/web/blob.cpp index c760b4ab..aa5b35ae 100644 --- a/builtins/web/blob.cpp +++ b/builtins/web/blob.cpp @@ -677,6 +677,8 @@ JSObject *Blob::create(JSContext *cx, UniqueChars data, size_t data_len, HandleS SetReservedSlot(self, static_cast(Slots::Type), JS::StringValue(type)); SetReservedSlot(self, static_cast(Slots::Endings), JS::Int32Value(LineEndings::Transparent)); SetReservedSlot(self, static_cast(Slots::Readers), JS::PrivateValue(new ReadersMap)); + SetReservedSlot(self, static_cast(Slots::Reserved1), JS::NullValue()); + SetReservedSlot(self, static_cast(Slots::Reserved2), JS::NullValue()); return self; } @@ -695,6 +697,8 @@ bool Blob::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { SetReservedSlot(self, static_cast(Slots::Endings), JS::Int32Value(LineEndings::Transparent)); SetReservedSlot(self, static_cast(Slots::Data), JS::PrivateValue(new ByteBuffer)); SetReservedSlot(self, static_cast(Slots::Readers), JS::PrivateValue(new ReadersMap)); + SetReservedSlot(self, static_cast(Slots::Reserved1), JS::NullValue()); + SetReservedSlot(self, static_cast(Slots::Reserved2), JS::NullValue()); // Walk the blob parts and append them to the blob's buffer. if (blobParts.isNull()) { diff --git a/builtins/web/blob.h b/builtins/web/blob.h index 857b789a..27c36943 100644 --- a/builtins/web/blob.h +++ b/builtins/web/blob.h @@ -51,7 +51,7 @@ class Blob : public TraceableBuiltinImpl { static const JSPropertySpec properties[]; static constexpr unsigned ctor_length = 0; - enum Slots { Data, Type, Endings, Readers, Reserved2, Reserved1, Count }; + enum Slots { Data, Type, Endings, Readers, Reserved1, Reserved2, Count }; enum LineEndings { Transparent, Native }; using HeapObj = Heap; diff --git a/builtins/web/file.cpp b/builtins/web/file.cpp index 435c085b..0b36b366 100644 --- a/builtins/web/file.cpp +++ b/builtins/web/file.cpp @@ -102,26 +102,26 @@ bool File::lastModified_get(JSContext *cx, unsigned argc, JS::Value *vp) { } bool File::is_instance(const JSObject *obj) { - return obj != nullptr && (JS::GetClass(obj) == &class_ || JS::GetClass(obj) == &Blob::class_); + return obj != nullptr + && JS::GetClass(obj) == &Blob::class_ + && !JS::GetReservedSlot( + (JSObject *)obj, + static_cast(ParentSlots::Name)).isNullOrUndefined(); } -bool File::is_instance(const Value val) { return val.isObject() && is_instance(&val.toObject()); } - -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)); +bool File::is_instance(const Value val) { + return val.isObject() && is_instance(&val.toObject()); +} +JSObject *File::create(JSContext *cx, HandleValue fileBits, HandleValue fileName, HandleValue opts) { RootedObject blob_ctor(cx, JS_GetConstructor(cx, Blob::proto_obj)); if (!blob_ctor) { - return false; + return nullptr; } RootedObject this_ctor(cx, JS_GetConstructor(cx, File::proto_obj)); if (!this_ctor) { - return false; + return nullptr; } MOZ_ASSERT(JS::IsConstructor(blob_ctor)); @@ -138,13 +138,13 @@ bool File::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { RootedValue blob_ctor_val(cx, JS::ObjectValue(*blob_ctor)); RootedObject self(cx); if (!JS::Construct(cx, blob_ctor_val, this_ctor, blob_args, &self)) { - return false; + return nullptr; } // 2. Let n be the fileName argument to the constructor. RootedString name(cx, JS::ToString(cx, fileName)); if (!name) { - return false; + return nullptr; } // 3. Process `FilePropertyBag` dictionary argument by running the following substeps: @@ -154,7 +154,7 @@ bool File::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { // milliseconds since the Unix Epoch. RootedValue lastModified(cx); if (!init_last_modified(cx, opts, &lastModified)) { - return false; + return nullptr; } // Return a new File object F such that: @@ -169,6 +169,21 @@ bool File::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { SetReservedSlot(self, static_cast(ParentSlots::Name), JS::StringValue(name)); SetReservedSlot(self, static_cast(ParentSlots::LastModified), lastModified); + return self; +} + +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, create(cx, fileBits, fileName, opts)); + if (!self) { + return false; + } + args.rval().setObject(*self); return true; } diff --git a/builtins/web/file.h b/builtins/web/file.h index a3aaf08a..05e7216b 100644 --- a/builtins/web/file.h +++ b/builtins/web/file.h @@ -24,6 +24,7 @@ class File : public BuiltinImpl { static bool is_instance(const JSObject *obj); static bool is_instance(const Value val); + static JSObject *create(JSContext *cx, HandleValue fileBits, HandleValue fileName, HandleValue opts); static bool init_class(JSContext *cx, HandleObject global); static bool constructor(JSContext *cx, unsigned argc, Value *vp); }; From 9ac74505d45035847d161cc5bc03df6d520e8895 Mon Sep 17 00:00:00 2001 From: Tomasz Andrzejak Date: Mon, 23 Dec 2024 21:13:15 +0100 Subject: [PATCH 10/10] Add FormData builtin --- builtins/web/form-data.cpp | 287 +++++++++++++++++++++++++++++++++++++ builtins/web/form-data.h | 55 +++++++ cmake/builtins.cmake | 1 + 3 files changed, 343 insertions(+) create mode 100644 builtins/web/form-data.cpp create mode 100644 builtins/web/form-data.h diff --git a/builtins/web/form-data.cpp b/builtins/web/form-data.cpp new file mode 100644 index 00000000..d1d1dfcf --- /dev/null +++ b/builtins/web/form-data.cpp @@ -0,0 +1,287 @@ +#include "blob.h" +#include "builtin.h" +#include "encode.h" +#include "file.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "form-data.h" + +namespace { + +using builtins::web::form_data::FormDataEntry; + +FormDataEntry entry_from_kv_pair(std::string name, HandleValue value) { + FormDataEntry entry; + entry.name = std::move(name); + entry.value = value; + + return entry; +} + +} // namespace + +namespace builtins { +namespace web { +namespace form_data { + +using blob::Blob; +using file::File; + +const JSFunctionSpec FormData::static_methods[] = { + JS_FS_END, +}; + +const JSPropertySpec FormData::static_properties[] = { + JS_PS_END, +}; + +const JSFunctionSpec FormData::methods[] = { + JS_FN("append", append, 0, JSPROP_ENUMERATE), + JS_FN("delete", remove, 0, JSPROP_ENUMERATE), + JS_FN("get", get, 0, JSPROP_ENUMERATE), + JS_FN("getAll", FormData::getAll, 0, JSPROP_ENUMERATE), + JS_FN("has", FormData::has, 0, JSPROP_ENUMERATE), + JS_FN("set", FormData::set, 0, JSPROP_ENUMERATE), + JS_FS_END, +}; + +const JSPropertySpec FormData::properties[] = { + JS_PS_END, +}; + +FormData::EntryList* FormData::entry_list(JSObject *self) { + MOZ_ASSERT(is_instance(self)); + + auto entries = static_cast( + JS::GetReservedSlot(self, static_cast(FormData::Slots::Entries)).toPrivate()); + + MOZ_ASSERT(entries); + return entries; +} + +bool FormData::append(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(2) + + RootedValue name(cx, args.get(0)); + RootedValue value(cx, args.get(1)); + RootedValue filename(cx, args.get(2)); + + if (!name.isString()) { + return false; + } + + auto chars = core::encode(cx, name); + if (!chars) { + return false; + } + + std::string name_str(chars.ptr.get(), chars.len); + auto entries = entry_list(self); + + switch(args.length()) { + case 2: { + if (!value.isString()) { + return false; + } + + auto entry = entry_from_kv_pair(std::move(name_str), value); + entries->append(entry); + break; + } + case 3: { + if (!value.isObject()) { + return false; + } + + RootedObject obj(cx, &value.toObject()); + + if (File::is_instance(obj)) { + auto entry = entry_from_kv_pair(std::move(name_str), value); + entries->append(entry); + } else if (Blob::is_instance(obj)) { + HandleValueArray arr = HandleValueArray(value); + RootedObject file_bits(cx, NewArrayObject(cx, arr)); + if (!file_bits) { + return false; + } + + RootedValue file_bits_val(cx, JS::ObjectValue(*file_bits)); + RootedValue opts_val(cx, JS::NullValue()); + RootedValue filename_val(cx); + + if (filename.isNullOrUndefined()) { + RootedString empty(cx, JS_NewStringCopyZ(cx, "blob")); + if (!empty) { + return false; + } + + RootedValue empty_val(cx, JS::StringValue(empty)); + filename_val = empty_val; + } else { + filename_val = filename; + } + + RootedObject file(cx, File::create(cx, file_bits_val, filename_val, opts_val)); + if (!file) { + return false; + } + + RootedValue file_val(cx, JS::ObjectValue(*file)); + auto entry = entry_from_kv_pair(std::move(name_str), file_val); + entries->append(entry); + } else { + return false; + } + + break; + } + default: + return false; + } + + return true; +} + +bool FormData::remove(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(1) + + RootedValue name(cx, args.get(0)); + + if (!name.isString()) { + return false; + } + + auto name_to_remove = core::encode(cx, name); + if (!name_to_remove) { + return false; + } + + entry_list(self)->eraseIf([&](const FormDataEntry &entry) { + return entry.name == std::string_view(name_to_remove); + }); + + return true; +} + +bool FormData::get(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(1) + + RootedValue name(cx, args.get(0)); + + if (!name.isString()) { + return false; + } + + auto name_to_get = core::encode(cx, name); + if (!name_to_get) { + return false; + } + + auto entries = entry_list(self); + auto it = std::find_if(entries->begin(), entries->end(), [&](const FormDataEntry &entry) { + return entry.name == std::string_view(name_to_get); + }); + + if (it != entries->end()) { + args.rval().set(it->value); + } else { + args.rval().setUndefined(); + } + + return true; +} + +bool FormData::getAll(JSContext *cx, unsigned argc, JS::Value *vp) { + return true; +} + +bool FormData::has(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(1) + + RootedValue name(cx, args.get(0)); + + if (!name.isString()) { + return false; + } + + auto name_to_find = core::encode(cx, name); + if (!name_to_find) { + return false; + } + + auto entries = entry_list(self); + auto it = std::find_if(entries->begin(), entries->end(), [&](const FormDataEntry &entry) { + return entry.name == std::string_view(name_to_find); + }); + + args.rval().setBoolean(it != entries->end()); + return true; +} + +bool FormData::set(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(1) + + RootedValue name(cx, args.get(0)); + RootedValue value(cx, args.get(1)); + + if (!name.isString()) { + return false; + } + + auto name_to_find = core::encode(cx, name); + if (!name_to_find) { + return false; + } + + auto entries = entry_list(self); + auto it = std::find_if(entries->begin(), entries->end(), [&](const FormDataEntry &entry) { + return entry.name == std::string_view(name_to_find); + }); + + if (it != entries->end()) { + it->value = value; + } + + return true; +} + +bool FormData::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { + CTOR_HEADER("FormData", 0); + + RootedObject self(cx, JS_NewObjectForConstructor(cx, &class_, args)); + + if (!self) { + return false; + } + + SetReservedSlot(self, static_cast(Slots::Entries), JS::PrivateValue(new EntryList)); + + args.rval().setObject(*self); + return true; +} + +void FormData::finalize(JS::GCContext *gcx, JSObject *self) { + MOZ_ASSERT(is_instance(self)); + auto entries = entry_list(self); + if (entries) { + free(entries); + } +} + +void FormData::trace(JSTracer *trc, JSObject *self) { + MOZ_ASSERT(is_instance(self)); + auto entries = entry_list(self); + entries->trace(trc); +} + +bool FormData::init_class(JSContext *cx, JS::HandleObject global) { + return init_class_impl(cx, global); +} + +bool install(api::Engine *engine) { + return FormData::init_class(engine->cx(), engine->global()); +} + +} // namespace form_data +} // namespace web +} // namespace builtins diff --git a/builtins/web/form-data.h b/builtins/web/form-data.h new file mode 100644 index 00000000..7c0cd3dc --- /dev/null +++ b/builtins/web/form-data.h @@ -0,0 +1,55 @@ +#ifndef BUILTINS_WEB_FORM_FDATA_H +#define BUILTINS_WEB_FORM_FDATA_H + +#include "builtin.h" + +namespace builtins { +namespace web { +namespace form_data { + +struct FormDataEntry { + std::string name; + JS::Heap value; + + void trace(JSTracer *trc) { + TraceEdge(trc, &value, "FormDataEntry value"); + } +}; + +class FormData : public TraceableBuiltinImpl { + static bool append(JSContext *cx, unsigned argc, JS::Value *vp); + static bool remove(JSContext *cx, unsigned argc, JS::Value *vp); + static bool get(JSContext *cx, unsigned argc, JS::Value *vp); + static bool getAll(JSContext *cx, unsigned argc, JS::Value *vp); + static bool has(JSContext *cx, unsigned argc, JS::Value *vp); + static bool set(JSContext *cx, unsigned argc, JS::Value *vp); + + using EntryList = JS::GCVector; + static EntryList* entry_list(JSObject *self); + +public: + static constexpr const char *class_name = "FormData"; + + static const JSFunctionSpec static_methods[]; + static const JSPropertySpec static_properties[]; + static const JSFunctionSpec methods[]; + static const JSPropertySpec properties[]; + + static constexpr unsigned ctor_length = 0; + + enum Slots { Entries, Count }; + + static bool init_class(JSContext *cx, HandleObject global); + static bool constructor(JSContext *cx, unsigned argc, Value *vp); + static void finalize(JS::GCContext *gcx, JSObject *self); + static void trace(JSTracer *trc, JSObject *self); +}; + +bool install(api::Engine *engine); + +} // namespace form_data +} // namespace web +} // namespace builtins + + +#endif // BUILTINS_WEB_FORM_FDATA_H diff --git a/cmake/builtins.cmake b/cmake/builtins.cmake index 474d4ffc..9e6c7fdb 100644 --- a/cmake/builtins.cmake +++ b/cmake/builtins.cmake @@ -19,6 +19,7 @@ add_builtin(builtins/web/structured-clone.cpp) add_builtin(builtins/web/base64.cpp) add_builtin(builtins/web/blob.cpp) add_builtin(builtins/web/file.cpp) +add_builtin(builtins/web/form-data.cpp) add_builtin( builtins::web::dom_exception SRC