diff --git a/src/browser/tests/net/form_data.html b/src/browser/tests/net/form_data.html
index 86c175779..413c7725a 100644
--- a/src/browser/tests/net/form_data.html
+++ b/src/browser/tests/net/form_data.html
@@ -898,3 +898,33 @@
testing.expectEqual('by-listener', fd.get('added'));
}
+
+
diff --git a/src/browser/webapi/net/FormData.zig b/src/browser/webapi/net/FormData.zig
index d4ffce43c..bf87bd789 100644
--- a/src/browser/webapi/net/FormData.zig
+++ b/src/browser/webapi/net/FormData.zig
@@ -25,6 +25,7 @@ const Frame = @import("../../Frame.zig");
const Form = @import("../element/html/Form.zig");
const Element = @import("../Element.zig");
const File = @import("../File.zig");
+const Blob = @import("../Blob.zig");
const KeyValueList = @import("../KeyValueList.zig");
const log = lp.log;
@@ -165,9 +166,65 @@ pub fn has(self: *const FormData, name: String) bool {
return false;
}
-pub fn set(self: *FormData, name: String, value: []const u8, exec: *Execution) !void {
+// The value half of the JS-facing append()/set(). `bytes` must stay last:
+// js.Bridge tries the JsApi classes first and maps anything else to a string,
+// so an earlier position would swallow File and Blob arguments.
+pub const EntryValue = union(enum) {
+ file: *File,
+ blob: *Blob,
+ bytes: []const u8,
+};
+
+pub fn jsSet(self: *FormData, name: String, value: EntryValue, filename: ?[]const u8, exec: *Execution) !void {
self.deleteByName(name, exec);
- return self.append(name.str(), value);
+ return self.jsAppend(name.str(), value, filename, exec);
+}
+
+// https://xhr.spec.whatwg.org/#create-an-entry
+pub fn jsAppend(self: *FormData, name: []const u8, value: EntryValue, filename: ?[]const u8, exec: *Execution) !void {
+ const entry_value: Entry.Value = switch (value) {
+ // A Blob that is not a File becomes a File named "blob", unless a
+ // filename was supplied.
+ .blob => |blob| blk: {
+ if (blob._type == .file and filename == null) {
+ const file = blob._type.file;
+ file.acquireRef();
+ break :blk .{ .file = file };
+ }
+ break :blk .{ .file = try fileFrom(blob, filename orelse "blob", exec.page) };
+ },
+ // A File keeps its bytes; a supplied filename means a new File over
+ // the same bytes rather than a rename of the caller's object.
+ .file => |file| blk: {
+ if (filename) |n| break :blk .{ .file = try fileFrom(file._proto, n, exec.page) };
+ file.acquireRef();
+ break :blk .{ .file = file };
+ },
+ .bytes => |b| .{ .string = try String.init(self._arena, b, .{}) },
+ };
+
+ try self._entries.append(self._arena, .{
+ .name = try String.init(self._arena, name, .{}),
+ .value = entry_value,
+ });
+}
+
+// Mirrors File.init — a Blob and File sharing one reference-counted arena —
+// but over bytes we already hold rather than JS parts. Returned at refcount 1:
+// the entry owns that reference and deleteByName releases it.
+fn fileFrom(source: *Blob, name: []const u8, page: *Page) !*File {
+ const blob = try Blob.initFromBytes(source._slice, source._mime, page);
+ errdefer blob.deinit(page);
+
+ const file = try blob._arena.create(File);
+ file.* = .{
+ ._proto = blob,
+ ._name = try blob._arena.dupe(u8, name),
+ ._last_modified = std.Io.Clock.now(.real, lp.io).toMilliseconds(),
+ };
+ blob._type = .{ .file = file };
+ blob.acquireRef();
+ return file;
}
pub fn append(self: *FormData, name: []const u8, value: []const u8) !void {
@@ -511,8 +568,8 @@ pub const JsApi = struct {
pub const constructor = bridge.constructor(FormData.init, .{});
pub const has = bridge.function(FormData.has, .{});
pub const get = bridge.function(FormData.get, .{});
- pub const set = bridge.function(FormData.set, .{});
- pub const append = bridge.function(FormData.append, .{});
+ pub const set = bridge.function(FormData.jsSet, .{});
+ pub const append = bridge.function(FormData.jsAppend, .{});
pub const getAll = bridge.function(FormData.getAll, .{});
pub const delete = bridge.function(FormData.delete, .{});
pub const keys = bridge.function(FormData.keys, .{});
@@ -599,8 +656,6 @@ test "FormData: multipart empty body" {
try testing.expectString("--B--\r\n", buf.written());
}
-const Blob = @import("../Blob.zig");
-
fn buildTestFile(arena: Allocator, page: *@import("../../Page.zig"), name: []const u8, mime: []const u8, body: []const u8) !*File {
const blob = try Blob.initFromBytes(body, mime, page);
blob.acquireRef();