diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index 1c7f0bfd..24faeade 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -4023,6 +4023,9 @@ pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.For @import("../id.zig").uuidv4(&boundary_buf); break :blk .{ .formdata = &boundary_buf }; } + if (std.ascii.eqlIgnoreCase(attr, "text/plain")) { + break :blk .plaintext; + } if (!std.ascii.eqlIgnoreCase(attr, "application/x-www-form-urlencoded")) { log.warn(.not_implemented, "FormData.encoding", .{ .encoding = attr }); } @@ -4051,6 +4054,9 @@ pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.For opts.header = switch (encoding) { .urlencode => "Content-Type: application/x-www-form-urlencoded", .formdata => |b| try std.fmt.allocPrintSentinel(arena, "Content-Type: multipart/form-data; boundary={s}", .{b}, 0), + // Per WHATWG HTML §4.10.21.6, text/plain submissions include the form's + // resolved encoding (accept-charset or document charset). + .plaintext => try std.fmt.allocPrintSentinel(arena, "Content-Type: text/plain; charset={s}", .{charset}, 0), }; } else { action = try URL.concatQueryString(arena, action, buf.written()); diff --git a/src/browser/webapi/net/FormData.zig b/src/browser/webapi/net/FormData.zig index fb4f49f3..026bfafb 100644 --- a/src/browser/webapi/net/FormData.zig +++ b/src/browser/webapi/net/FormData.zig @@ -163,6 +163,7 @@ pub const EncType = union(enum) { urlencode, // Boundary delimiter; caller owns the bytes (must outlive the write). formdata: []const u8, + plaintext, }; pub const WriteOpts = struct { @@ -175,6 +176,7 @@ pub fn write(self: *const FormData, opts: WriteOpts, writer: *std.Io.Writer) !vo switch (opts.encoding) { .urlencode => return self.urlEncode(opts, writer), .formdata => |boundary| return self.multipartEncode(boundary, writer), + .plaintext => return self.plaintextEncode(writer), } } @@ -195,6 +197,20 @@ fn urlEncodeEntry(entry: *const Entry, opts: WriteOpts, writer: *std.Io.Writer) try KeyValueList.urlEncodeFormValue(entry.value.asString(), opts.allocator, opts.charset, writer); } +/// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#text/plain-encoding-algorithm +/// +/// For each entry: name, "=", value, CRLF. No URL-encoding, no escaping. Per the +/// spec this is a low-fidelity encoding intended for human-readable values; a +/// value containing "=" or CRLF produces an ambiguous wire format, by design. +fn plaintextEncode(self: *const FormData, writer: *std.Io.Writer) !void { + for (self._entries.items) |*entry| { + try writer.writeAll(entry.name.str()); + try writer.writeByte('='); + try writer.writeAll(entry.value.asString()); + try writer.writeAll("\r\n"); + } +} + fn multipartEncode(self: *const FormData, boundary: []const u8, writer: *std.Io.Writer) !void { for (self._entries.items) |*entry| { try multipartEncodeEntry(entry, boundary, writer); @@ -461,3 +477,42 @@ test "FormData: multipart empty body" { try testing.expectString("--B--\r\n", buf.written()); } + +test "FormData: plaintext write" { + const allocator = testing.arena_allocator; + + var fd = FormData{ + ._arena = allocator, + ._entries = .empty, + }; + try fd.append("name", "John"); + try fd.append("note", "two\r\nlines"); + try fd.append("equals", "a=b"); + + var buf = std.Io.Writer.Allocating.init(allocator); + try fd.write(.{ .encoding = .plaintext, .allocator = allocator }, &buf.writer); + + // Per WHATWG HTML text/plain encoding algorithm: name=value CRLF per entry. + // Values containing "=" or CRLF are written verbatim — the spec accepts that + // text/plain is a low-fidelity, lossy encoding for human-readable content. + try testing.expectString( + "name=John\r\n" ++ + "note=two\r\nlines\r\n" ++ + "equals=a=b\r\n", + buf.written(), + ); +} + +test "FormData: plaintext empty body" { + const allocator = testing.arena_allocator; + + var fd = FormData{ + ._arena = allocator, + ._entries = .empty, + }; + + var buf = std.Io.Writer.Allocating.init(allocator); + try fd.write(.{ .encoding = .plaintext, .allocator = allocator }, &buf.writer); + + try testing.expectString("", buf.written()); +}