Cleaner multipart handling
This commit is contained in:
parent
6e56775d61
commit
e6f57495c0
1 changed files with 119 additions and 10 deletions
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue