Cleaner multipart handling

This commit is contained in:
jaina heartles 2022-12-02 22:20:24 -08:00
parent 6e56775d61
commit e6f57495c0

View file

@ -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);
}