Skip to content

Commit

Permalink
Add FormData builtin
Browse files Browse the repository at this point in the history
  • Loading branch information
andreiltd committed Jan 9, 2025
1 parent 15f709f commit 0a65a1e
Show file tree
Hide file tree
Showing 17 changed files with 636 additions and 2 deletions.
334 changes: 334 additions & 0 deletions builtins/web/form-data.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
#include "blob.h"
#include "builtin.h"
#include "encode.h"
#include "file.h"
#include "form-data.h"

#include "host_api.h"
#include "js/TypeDecls.h"
#include "js/Value.h"

namespace {

using builtins::web::form_data::FormData;
using builtins::web::form_data::FormDataEntry;

FormDataEntry entry_from_kv_pair(std::string_view name, HandleValue value) {
FormDataEntry entry;
entry.name = name;
entry.value = value;

return entry;
}

bool name_from_args(JSContext* cx, const JS::CallArgs& args, host_api::HostString& out) {
JS::RootedValue val(cx, args[0]);
if (!val.isString()) {
return false;
}

auto encoded = core::encode(cx, val);
if (!encoded) {
return false;
}

out = std::move(encoded);
return true;
}

} // namespace

namespace builtins {
namespace web {
namespace form_data {

using host_api::HostString;
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<EntryList *>(
JS::GetReservedSlot(self, static_cast<size_t>(FormData::Slots::Entries)).toPrivate());

MOZ_ASSERT(entries);
return entries;
}

bool FormData::append(JSContext* cx, HandleObject self, std::string_view key, HandleValue value) {
auto entries = entry_list(self);

RootedString str(cx, JS::ToString(cx, value));
if (!str) {
return false;
}

RootedValue str_val(cx, StringValue(str));
auto entry = entry_from_kv_pair(key, str_val);

entries->append(entry);
return true;
}

bool FormData::append(JSContext* cx, HandleObject self, std::string_view key, HandleValue value, HandleValue filename) {
if (!value.isObject()) {
return false;
}

auto entries = entry_list(self);
RootedObject obj(cx, &value.toObject());

if (File::is_instance(obj)) {
auto entry = entry_from_kv_pair(key, 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 default_name(cx, JS_NewStringCopyZ(cx, "blob"));
if (!default_name) {
return false;
}

RootedValue default_name_val(cx, JS::StringValue(default_name));
filename_val = default_name_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(key, file_val);
entries->append(entry);
}

return true;
}

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));

HostString name_to_add;
if (!name_from_args(cx, args, name_to_add)) {
return false;
}

switch(args.length()) {
case 2:
return append(cx, self, name_to_add, value);
default:
case 3:
return append(cx, self, name_to_add, value, filename);
return false;
}
}

bool FormData::remove(JSContext *cx, unsigned argc, JS::Value *vp) {
METHOD_HEADER(1)

RootedValue name(cx, args.get(0));
HostString name_to_remove;
if (!name_from_args(cx, args, 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));
HostString name_to_get;
if (!name_from_args(cx, args, 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().setNull();
}

return true;
}

bool FormData::getAll(JSContext *cx, unsigned argc, JS::Value *vp) {
METHOD_HEADER(1)

HostString name_to_get;
if (!name_from_args(cx, args, name_to_get)) {
return false;
}

auto entries = entry_list(self);

JS::RootedObject array(cx, JS::NewArrayObject(cx, 0));
if (!array) {
return false;
}

uint32_t index = 0;
for (auto &entry : *entries) {
if (entry.name == std::string_view(name_to_get)) {
JS::RootedValue val(cx, entry.value);
if (!JS_DefineElement(cx, array, index++, val, JSPROP_ENUMERATE)) {
return false;
}
}
}

args.rval().setObject(*array);
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));
RootedValue filename(cx, args.get(2));

HostString name_to_find;
if (!name_from_args(cx, args, 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;
} else {
switch(args.length()) {
case 2:
return append(cx, self, name_to_find, value);
default:
case 3:
return append(cx, self, name_to_find, value, filename);
}
}
}

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

// The FormData constructor optionally takes HTMLFormElement and HTMLElement as parameters.
// As we do not support DOM we throw if the first parameter is not undefined.
//
// See https://min-common-api.proposal.wintercg.org/#issue-92f53c35
if (!args.get(0).isNullOrUndefined()) {
return api::throw_error(cx, api::Errors::TypeError, "FormData.constructor", "form", "be null or undefined");
}

RootedObject self(cx, JS_NewObjectForConstructor(cx, &class_, args));

if (!self) {
return false;
}

SetReservedSlot(self, static_cast<uint32_t>(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
56 changes: 56 additions & 0 deletions builtins/web/form-data.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#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<JS::Value> value;

void trace(JSTracer *trc) { TraceEdge(trc, &value, "FormDataEntry value"); }
};

class FormData : public TraceableBuiltinImpl<FormData> {
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);

static bool append(JSContext *cx, HandleObject self, std::string_view key, HandleValue val);
static bool append(JSContext *cx, HandleObject self, std::string_view key, HandleValue val,
HandleValue filename);

using EntryList = JS::GCVector<FormDataEntry, 0, js::SystemAllocPolicy>;
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
1 change: 1 addition & 0 deletions cmake/builtins.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 0a65a1e

Please sign in to comment.