Skip to content
Open
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
8 changes: 1 addition & 7 deletions src/runtime/webcore/Blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3471,10 +3471,7 @@ use self::write_file::{WriteFilePromise, WriteFileWaitFromLockedValueTask};
use bun_bundler::options_impl::LoaderExt as _;
use bun_jsc::JsClass as _;

/// Local mirror of `jsc.DOMFormData.FormDataEntry` (`union(enum) { string, file }`).
/// `bun_jsc::dom_form_data::FormDataEntry` carries `&Blob` (immutable) but
/// `FormDataContext::on_entry` needs `&mut Blob` to call `resolve_size()`, so
/// we drive the C++ `DOMFormData__forEach` directly with this mutable variant.
/// One `DOMFormData` entry, as the `DOMFormData__forEach` callback receives it.
pub(crate) enum FormDataEntry<'a> {
String(EncodedSlice<'a>),
File {
Expand Down Expand Up @@ -3662,9 +3659,6 @@ impl FormDataContext<'_> {
joiner.push_static(b"\r\n\r\n");

if blob.store.get().is_some() {
if blob.size.get() == MAX_SIZE {
blob.resolve_size();
}
let store = blob
.store
.get()
Expand Down
66 changes: 65 additions & 1 deletion test/js/web/html/FormData.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it, test } from "bun:test";
import { bunEnv, bunExe, isASAN, isDebug } from "harness";
import { bunEnv, bunExe, isASAN, isDebug, isLinux, isWindows, tempDir } from "harness";
import { readFileSync } from "node:fs";
import { join } from "path";

describe("FormData", () => {
Expand Down Expand Up @@ -1004,3 +1005,66 @@ describe("USVString conversion of lone surrogates", () => {
expect(formData.get("\uFFFD")).toBeNull();
});
});

describe("FormData with Bun.file() parts", () => {
async function roundTrip(formData: FormData): Promise<FormData> {
const res = new Response(formData);
const contentType = res.headers.get("content-type")!;
const wire = await res.bytes();
return new Response(wire, { headers: { "content-type": contentType } }).formData();
}

// procfs files are regular files whose st_size is 0 but that have content.
it.skipIf(!isLinux)("serializes a procfs Bun.file() part with its content", async () => {
const path = "/proc/version";
const expected = readFileSync(path, "utf8");
expect(expected.length).toBeGreaterThan(0);

const formData = new FormData();
formData.append("f", Bun.file(path), "version.txt");
formData.append("mem", new Blob(["in-memory"]), "mem.txt");

const back = await roundTrip(formData);
expect(await (back.get("f") as File).text()).toBe(expected);
expect(await (back.get("mem") as File).text()).toBe("in-memory");
});

it("serializes a sliced Bun.file() part with only the slice", async () => {
using dir = tempDir("formdata-slice", { "a.txt": "0123456789" });
const file = Bun.file(join(String(dir), "a.txt"));

const formData = new FormData();
formData.append("mid", file.slice(2, 5), "mid.txt");
formData.append("tail", file.slice(7), "tail.txt");
formData.append("all", file, "all.txt");

const back = await roundTrip(formData);
expect(await (back.get("mid") as File).text()).toBe("234");
expect(await (back.get("tail") as File).text()).toBe("789");
expect(await (back.get("all") as File).text()).toBe("0123456789");
});

// /dev/zero has no EOF, so "read to EOF" applies to regular files only. The
// part is read on the JS thread: a read with no bound never returns, so the
// serialize runs in a child that this process can outlive.
it.skipIf(isWindows)("serializes a bounded part for a device with no EOF", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`const formData = new FormData();
formData.append("f", Bun.file("/dev/zero"), "zero.bin");
console.log((await new Response(formData).bytes()).length);`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
const serialized = Number(stdout);
expect(serialized).toBeGreaterThan(0);
expect(serialized).toBeLessThanOrEqual(1024 * 1024);
expect(exitCode).toBe(0);
});
});
Loading