diff --git a/src/http/multipart.zig b/src/http/multipart.zig index 527148f..9b9e01b 100644 --- a/src/http/multipart.zig +++ b/src/http/multipart.zig @@ -9,6 +9,7 @@ pub fn MultipartStream(comptime ReaderType: type) type { return struct { const Multipart = @This(); + pub const BaseReader = ReaderType; pub const PartReader = std.io.Reader(*Part, ReaderType.Error, Part.read); stream: std.io.PeekStream(.{ .Static = read_ahead }, ReaderType), @@ -101,8 +102,51 @@ pub fn openMultipart(boundary: []const u8, reader: anytype) !MultipartStream(@Ty return stream; } +const MultipartFormField = struct { + name: []const u8, + value: []const u8, + + filename: ?[]const u8 = null, + content_type: ?[]const u8 = null, +}; + +pub fn MultipartForm(comptime ReaderType: type) type { + return struct { + stream: MultipartStream(ReaderType), + + pub fn next(self: *@This(), alloc: std.mem.Allocator) !?MultipartFormField { + var part = (try self.stream.next(alloc)) orelse return null; + defer part.close(); + + const disposition = part.fields.get("Content-Disposition") orelse return error.MissingDisposition; + + if (!std.ascii.eqlIgnoreCase(fields.getParam(disposition, null).?, "form-data")) return error.BadDisposition; + const name = try util.deepClone(alloc, fields.getParam(disposition, "name") orelse return error.BadDisposition); + errdefer util.deepFree(alloc, name); + const filename = try util.deepClone(alloc, fields.getParam(disposition, "filename")); + errdefer util.deepFree(alloc, filename); + const content_type = try util.deepClone(alloc, part.fields.get("Content-Type")); + errdefer util.deepFree(alloc, content_type); + + const value = try part.reader().readAllAlloc(alloc, 1 << 32); + + return MultipartFormField{ + .name = name, + .value = value, + + .filename = filename, + .content_type = content_type, + }; + } + }; +} + +pub fn openForm(multipart_stream: anytype) MultipartForm(@TypeOf(multipart_stream).BaseReader) { + return .{ .stream = multipart_stream }; +} + pub fn parseFormData(comptime T: type, boundary: []const u8, reader: anytype, alloc: std.mem.Allocator) !T { - var multipart = try openMultipart(boundary, reader); + var form = openForm(try openMultipart(boundary, reader)); var ds = util.Deserializer(T){}; defer { @@ -112,16 +156,16 @@ pub fn parseFormData(comptime T: type, boundary: []const u8, reader: anytype, al } } while (true) { - var part = (try multipart.next(alloc)) orelse break; - defer part.close(); + var part = (try form.next(alloc)) orelse break; + errdefer util.deepFree(alloc, part); - const disposition = part.fields.get("Content-Disposition") orelse return error.InvalidForm; + try ds.setSerializedField(part.name, part.value); - const name = fields.getParam(disposition, "name") orelse return error.InvalidForm; + alloc.free(part.name); - const value = try part.reader().readAllAlloc(alloc, 1 << 32); - errdefer alloc.free(value); - try ds.setSerializedField(name, value); + // TODO: + if (part.content_type) |v| alloc.free(v); + if (part.filename) |v| alloc.free(v); } return try ds.finish(alloc); @@ -204,6 +248,72 @@ test "MultipartStream" { ); } +test "MultipartForm" { + const testCase = struct { + fn case(comptime body: []const u8, boundary: []const u8, expected_parts: []const MultipartFormField) !void { + var src = std.io.StreamSource{ + .const_buffer = std.io.fixedBufferStream(body), + }; + + var form = openForm(try openMultipart(boundary, src.reader())); + + for (expected_parts) |expected| { + var data = try form.next(std.testing.allocator) orelse return error.TestExpectedEqual; + defer util.deepFree(std.testing.allocator, data); + + try util.testing.expectDeepEqual(expected, data); + } + + try std.testing.expect(try form.next(std.testing.allocator) == null); + } + }.case; + + try testCase( + util.comptimeToCrlf( + \\--abcd + \\Content-Disposition: form-data; name=foo + \\ + \\content + \\--abcd-- + \\ + ), + "abcd", + &.{.{ .name = "foo", .value = "content" }}, + ); + try testCase( + util.comptimeToCrlf( + \\--abcd + \\Content-Disposition: form-data; name=foo + \\ + \\content + \\--abcd + \\Content-Disposition: form-data; name=bar + \\Content-Type: blah + \\ + \\abcd + \\--abcd + \\Content-Disposition: form-data; name=baz; filename="myfile.txt" + \\Content-Type: text/plain + \\ + \\ --abcd + \\ + \\--abcd-- + \\ + ), + "abcd", + &.{ + .{ .name = "foo", .value = "content" }, + .{ .name = "bar", .value = "abcd", .content_type = "blah" }, + .{ + .name = "baz", + .value = " --abcd\r\n", + .content_type = "text/plain", + .filename = "myfile.txt", + }, + }, + ); +} + test "parseFormData" { const body = util.comptimeToCrlf( \\--abcd @@ -213,10 +323,9 @@ test "parseFormData" { \\--abcd-- \\ ); - if (true) return error.SkipZigTest; // TODO var src = std.io.StreamSource{ .const_buffer = std.io.fixedBufferStream(body) }; const val = try parseFormData(struct { foo: []const u8, }, "abcd", src.reader(), std.testing.allocator); - _ = val; + util.deepFree(std.testing.allocator, val); }