Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -925,7 +925,7 @@ headers:
rm -f /tmp/build-jsc-headers src/bun.js/bindings/headers.zig
touch src/bun.js/bindings/headers.zig
$(ZIG) build headers-obj
$(CXX) $(PLATFORM_LINKER_FLAGS) $(JSC_FILES_DEBUG) ${ICU_FLAGS} $(DEBUG_IO_FILES) $(BUN_LLD_FLAGS_WITHOUT_JSC) -g $(DEBUG_BIN)/headers.o -W -o /tmp/build-jsc-headers -lc;
$(CXX) $(PLATFORM_LINKER_FLAGS) $(JSC_FILES_DEBUG) ${ICU_FLAGS} $(BUN_LLD_FLAGS_WITHOUT_JSC) -g $(DEBUG_BIN)/headers.o -W -o /tmp/build-jsc-headers -lc;
/tmp/build-jsc-headers
$(ZIG) translate-c src/bun.js/bindings/headers.h > src/bun.js/bindings/headers.zig
$(BUN_OR_NODE) misctools/headers-cleaner.js
Expand Down
88 changes: 85 additions & 3 deletions packages/bun-types/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ interface BlobInterface {
text(): Promise<string>;
arrayBuffer(): Promise<ArrayBuffer>;
json<TJSONReturnType = unknown>(): Promise<TJSONReturnType>;
formData(): Promise<FormData>;
}

type BlobPart = string | Blob | BufferSource | ArrayBuffer;
Expand Down Expand Up @@ -501,6 +502,51 @@ type ResponseType =
| "opaque"
| "opaqueredirect";

type FormDataEntryValue = Blob | string;

/** Provides a way to easily construct a set of key/value pairs representing
* form fields and their values, which can then be easily sent using the
* XMLHttpRequest.send() method. It uses the same format a form would use if the
* encoding type were set to "multipart/form-data".
*/
interface FormData {
/**
* Appends a new value onto an existing key inside a FormData object, or adds
* the key if it does not already exist.
*
* @param name The name of the field whose data is contained in value.
* @param value The field's value.
* @param fileName The filename reported to the server.
*
* ## Upload a file
* ```ts
* const formData = new FormData();
* formData.append("username", "abc123");
* formData.append("avatar", Bun.file("avatar.png"), "avatar.png");
* await fetch("https://example.com", { method: "POST", body: formData });
* ```
*/
append(name: string, value: string | Blob, fileName?: string): void;
delete(name: string): void;
get(name: string): FormDataEntryValue | null;
getAll(name: string): FormDataEntryValue[];
has(name: string): boolean;
set(name: string, value: string | Blob, fileName?: string): void;
keys(): IterableIterator<string>;
values(): IterableIterator<string>;
entries(): IterableIterator<[string, FormDataEntryValue]>;
[Symbol.iterator](): IterableIterator<[string, FormDataEntryValue]>;
forEach(
callback: (value: FormDataEntryValue, key: string, parent: this) => void,
thisArg?: any,
): void;
}

declare var FormData: {
prototype: FormData;
new (): FormData;
};

declare class Blob implements BlobInterface {
/**
* Create a new [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob)
Expand Down Expand Up @@ -545,6 +591,20 @@ declare class Blob implements BlobInterface {
*/
json<TJSONReturnType = unknown>(): Promise<TJSONReturnType>;

/**
* Read the data from the blob as a {@link FormData} object.
*
* This first decodes the data from UTF-8, then parses it as a
* `multipart/form-data` body or a `application/x-www-form-urlencoded` body.
*
* The `type` property of the blob is used to determine the format of the
* body.
*
* This is a non-standard addition to the `Blob` API, to make it conform more
* closely to the `BodyMixin` API.
*/
formData(): Promise<FormData>;

type: string;
size: number;
}
Expand Down Expand Up @@ -576,7 +636,7 @@ interface ResponseInit {
*/
declare class Response implements BlobInterface {
constructor(
body?: ReadableStream | BlobPart | BlobPart[] | null,
body?: ReadableStream | BlobPart | BlobPart[] | null | FormData,
options?: ResponseInit,
);

Expand Down Expand Up @@ -691,6 +751,18 @@ declare class Response implements BlobInterface {
*/
blob(): Promise<Blob>;

/**
* Read the data from the Response as a {@link FormData} object.
*
* This first decodes the data from UTF-8, then parses it as a
* `multipart/form-data` body or a `application/x-www-form-urlencoded` body.
*
* If no `Content-Type` header is present, the promise will be rejected.
*
* @returns Promise<FormData> - The body of the response as a {@link FormData}.
*/
formData(): Promise<FormData>;

readonly ok: boolean;
readonly redirected: boolean;
/**
Expand Down Expand Up @@ -752,10 +824,10 @@ type ReferrerPolicy =
| "strict-origin"
| "strict-origin-when-cross-origin"
| "unsafe-url";
type RequestInfo = Request | string;
type RequestInfo = Request | string | RequestInit;

type BodyInit = ReadableStream | XMLHttpRequestBodyInit;
type XMLHttpRequestBodyInit = Blob | BufferSource | string;
type XMLHttpRequestBodyInit = Blob | BufferSource | string | FormData;
type ReadableStreamController<T> = ReadableStreamDefaultController<T>;
type ReadableStreamDefaultReadResult<T> =
| ReadableStreamDefaultReadValueResult<T>
Expand Down Expand Up @@ -991,6 +1063,16 @@ declare class Request implements BlobInterface {

/** Copy the Request object into a new Request, including the body */
clone(): Request;

/**
* Read the body from the Request as a {@link FormData} object.
*
* This first decodes the data from UTF-8, then parses it as a
* `multipart/form-data` body or a `application/x-www-form-urlencoded` body.
*
* @returns Promise<FormData> - The body of the request as a {@link FormData}.
*/
formData(): Promise<FormData>;
}

declare interface Crypto {
Expand Down
1 change: 1 addition & 0 deletions src/bun.js/api/bun/dns_resolver.zig
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,7 @@ pub const GetAddrInfoRequest = struct {
pub fn onMachportChange(this: *GetAddrInfoRequest) void {
if (comptime !Environment.isMac)
unreachable;
bun.JSC.markBinding(@src());

if (!getaddrinfo_send_reply(this.backend.libinfo.machport.?, JSC.DNS.LibInfo.getaddrinfo_async_handle_reply().?)) {
log("onMachportChange: getaddrinfo_send_reply failed", .{});
Expand Down
6 changes: 3 additions & 3 deletions src/bun.js/bindings/BunClientData.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class DOMWrapperWorld;
#include "wtf/RefPtr.h"
#include "JavaScriptCore/WeakInlines.h"
#include "JavaScriptCore/IsoSubspacePerVM.h"

#include "wtf/StdLibExtras.h"
#include "WebCoreJSBuiltins.h"

namespace Zig {
Expand Down Expand Up @@ -148,7 +148,7 @@ ALWAYS_INLINE JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm, GetClient
uniqueSubspace = makeUnique<JSC::IsoSubspace> ISO_SUBSPACE_INIT(heap, heap.cellHeapCellType, T);
}
space = uniqueSubspace.get();
setServer(subspaces, uniqueSubspace);
setServer(subspaces, WTFMove(uniqueSubspace));

IGNORE_WARNINGS_BEGIN("unreachable-code")
IGNORE_WARNINGS_BEGIN("tautological-compare")
Expand All @@ -162,7 +162,7 @@ ALWAYS_INLINE JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm, GetClient

auto uniqueClientSubspace = makeUnique<JSC::GCClient::IsoSubspace>(*space);
auto* clientSpace = uniqueClientSubspace.get();
setClient(clientSubspaces, uniqueClientSubspace);
setClient(clientSubspaces, WTFMove(uniqueClientSubspace));
return clientSpace;
}

Expand Down
4 changes: 2 additions & 2 deletions src/bun.js/bindings/CallSite.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ class CallSite final : public JSC::JSNonFinalObject {
return WebCore::subspaceForImpl<CallSite, UseCustomHeapCellType::No>(
vm,
[](auto& spaces) { return spaces.m_clientSubspaceForCallSite.get(); },
[](auto& spaces, auto&& space) { spaces.m_clientSubspaceForCallSite = WTFMove(space); },
[](auto& spaces, auto&& space) { spaces.m_clientSubspaceForCallSite = std::forward<decltype(space)>(space); },
[](auto& spaces) { return spaces.m_subspaceForCallSite.get(); },
[](auto& spaces, auto&& space) { spaces.m_subspaceForCallSite = WTFMove(space); });
[](auto& spaces, auto&& space) { spaces.m_subspaceForCallSite = std::forward<decltype(space)>(space); });
}

JSC::JSValue thisValue() const { return m_thisValue.get(); }
Expand Down
200 changes: 200 additions & 0 deletions src/bun.js/bindings/DOMFormData.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*
* Copyright (C) 2010 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#include "config.h"
#include "DOMFormData.h"
#include "wtf/URLParser.h"

namespace WebCore {

DOMFormData::DOMFormData(ScriptExecutionContext* context)
: ContextDestructionObserver(context)
{
}

Ref<DOMFormData> DOMFormData::create(ScriptExecutionContext* context)
{
return adoptRef(*new DOMFormData(context));
}

Ref<DOMFormData> DOMFormData::create(ScriptExecutionContext* context, StringView urlEncodedString)
{
auto newFormData = adoptRef(*new DOMFormData(context));
for (auto& entry : WTF::URLParser::parseURLEncodedForm(urlEncodedString)) {
newFormData->append(entry.key, entry.value);
}

return newFormData;
}

String DOMFormData::toURLEncodedString()
{
WTF::URLParser::URLEncodedForm form;
form.reserveInitialCapacity(m_items.size());
for (auto& item : m_items) {
if (auto value = std::get_if<String>(&item.data))
form.append({ item.name, *value });
}

return WTF::URLParser::serialize(form);
}

extern "C" void DOMFormData__forEach(DOMFormData* form, void* context, void (*callback)(void* context, ZigString*, void*, ZigString*, uint8_t))
{
for (auto& item : form->items()) {
auto name = toZigString(item.name);
if (auto value = std::get_if<String>(&item.data)) {
auto value_ = toZigString(*value);
callback(context, &name, &value_, nullptr, 0);
} else if (auto value = std::get_if<RefPtr<Blob>>(&item.data)) {
auto filename = toZigString(value->get()->fileName());
callback(context, &name, value->get()->impl(), &filename, 1);
}
}
}

Ref<DOMFormData> DOMFormData::clone() const
{
auto newFormData = adoptRef(*new DOMFormData(scriptExecutionContext()));
newFormData->m_items = m_items;

return newFormData;
}

// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#create-an-entry
static auto createStringEntry(const String& name, const String& value) -> DOMFormData::Item
{
return {
replaceUnpairedSurrogatesWithReplacementCharacter(String(name)),
replaceUnpairedSurrogatesWithReplacementCharacter(String(value)),
};
}

void DOMFormData::append(const String& name, const String& value)
{
m_items.append(createStringEntry(name, value));
}

void DOMFormData::append(const String& name, RefPtr<Blob> blob, const String& filename)
{
blob->setFileName(replaceUnpairedSurrogatesWithReplacementCharacter(String(filename)));
m_items.append({ replaceUnpairedSurrogatesWithReplacementCharacter(String(name)), blob });
}
void DOMFormData::remove(const String& name)
{
m_items.removeAllMatching([&name](const auto& item) {
return item.name == name;
});
}

auto DOMFormData::get(const String& name) -> std::optional<FormDataEntryValue>
{
for (auto& item : m_items) {
if (item.name == name)
return item.data;
}

return std::nullopt;
}

auto DOMFormData::getAll(const String& name) -> Vector<FormDataEntryValue>
{
Vector<FormDataEntryValue> result;

for (auto& item : m_items) {
if (item.name == name)
result.append(item.data);
}

return result;
}

bool DOMFormData::has(const String& name)
{
for (auto& item : m_items) {
if (item.name == name)
return true;
}

return false;
}

void DOMFormData::set(const String& name, const String& value)
{
set(name, { name, value });
}

void DOMFormData::set(const String& name, RefPtr<Blob> blob, const String& filename)
{
blob->setFileName(filename);
set(name, { name, blob });
}

void DOMFormData::set(const String& name, Item&& item)
{
std::optional<size_t> initialMatchLocation;

// Find location of the first item with a matching name.
for (size_t i = 0; i < m_items.size(); ++i) {
if (name == m_items[i].name) {
initialMatchLocation = i;
break;
}
}

if (initialMatchLocation) {
m_items[*initialMatchLocation] = WTFMove(item);

m_items.removeAllMatching([&name](const auto& item) {
return item.name == name;
},
*initialMatchLocation + 1);
return;
}

m_items.append(WTFMove(item));
}

DOMFormData::Iterator::Iterator(DOMFormData& target)
: m_target(target)
{
}

std::optional<KeyValuePair<String, DOMFormData::FormDataEntryValue>> DOMFormData::Iterator::next()
{
auto& items = m_target->items();
if (m_index >= items.size())
return std::nullopt;

auto& item = items[m_index++];
return makeKeyValuePair(item.name, item.data);
}

} // namespace WebCore
Loading