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 {
|
return struct {
|
||||||
const Multipart = @This();
|
const Multipart = @This();
|
||||||
|
|
||||||
|
pub const BaseReader = ReaderType;
|
||||||
pub const PartReader = std.io.Reader(*Part, ReaderType.Error, Part.read);
|
pub const PartReader = std.io.Reader(*Part, ReaderType.Error, Part.read);
|
||||||
|
|
||||||
stream: std.io.PeekStream(.{ .Static = read_ahead }, ReaderType),
|
stream: std.io.PeekStream(.{ .Static = read_ahead }, ReaderType),
|
||||||
|
@ -101,8 +102,51 @@ pub fn openMultipart(boundary: []const u8, reader: anytype) !MultipartStream(@Ty
|
||||||
return stream;
|
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 {
|
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){};
|
var ds = util.Deserializer(T){};
|
||||||
defer {
|
defer {
|
||||||
|
@ -112,16 +156,16 @@ pub fn parseFormData(comptime T: type, boundary: []const u8, reader: anytype, al
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while (true) {
|
while (true) {
|
||||||
var part = (try multipart.next(alloc)) orelse break;
|
var part = (try form.next(alloc)) orelse break;
|
||||||
defer part.close();
|
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);
|
// TODO:
|
||||||
errdefer alloc.free(value);
|
if (part.content_type) |v| alloc.free(v);
|
||||||
try ds.setSerializedField(name, value);
|
if (part.filename) |v| alloc.free(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
return try ds.finish(alloc);
|
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" {
|
test "parseFormData" {
|
||||||
const body = util.comptimeToCrlf(
|
const body = util.comptimeToCrlf(
|
||||||
\\--abcd
|
\\--abcd
|
||||||
|
@ -213,10 +323,9 @@ test "parseFormData" {
|
||||||
\\--abcd--
|
\\--abcd--
|
||||||
\\
|
\\
|
||||||
);
|
);
|
||||||
if (true) return error.SkipZigTest; // TODO
|
|
||||||
var src = std.io.StreamSource{ .const_buffer = std.io.fixedBufferStream(body) };
|
var src = std.io.StreamSource{ .const_buffer = std.io.fixedBufferStream(body) };
|
||||||
const val = try parseFormData(struct {
|
const val = try parseFormData(struct {
|
||||||
foo: []const u8,
|
foo: []const u8,
|
||||||
}, "abcd", src.reader(), std.testing.allocator);
|
}, "abcd", src.reader(), std.testing.allocator);
|
||||||
_ = val;
|
util.deepFree(std.testing.allocator, val);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue