Multipart/form-data

This commit is contained in:
jaina heartles 2022-12-02 21:49:27 -08:00
parent 0b13f210c7
commit 6e56775d61
2 changed files with 106 additions and 100 deletions

View File

@ -18,6 +18,7 @@ const util = @import("util");
const http = @import("./lib.zig");
const urlencode = @import("./urlencode.zig");
const json_utils = @import("./json.zig");
const fields = @import("./fields.zig");
/// Takes an iterable of middlewares and chains them together.
pub fn apply(middlewares: anytype) Apply(@TypeOf(middlewares)) {
@ -29,20 +30,20 @@ pub fn Apply(comptime Middlewares: type) type {
return ApplyInternal(std.meta.fields(Middlewares));
}
fn ApplyInternal(comptime fields: []const std.builtin.Type.StructField) type {
if (fields.len == 0) return void;
fn ApplyInternal(comptime which: []const std.builtin.Type.StructField) type {
if (which.len == 0) return void;
return HandlerList(
fields[0].field_type,
ApplyInternal(fields[1..]),
which[0].field_type,
ApplyInternal(which[1..]),
);
}
fn applyInternal(middlewares: anytype, comptime fields: []const std.builtin.Type.StructField) ApplyInternal(fields) {
if (fields.len == 0) return {};
fn applyInternal(middlewares: anytype, comptime which: []const std.builtin.Type.StructField) ApplyInternal(which) {
if (which.len == 0) return {};
return .{
.first = @field(middlewares, fields[0].name),
.next = applyInternal(middlewares, fields[1..]),
.first = @field(middlewares, which[0].name),
.next = applyInternal(middlewares, which[1..]),
};
}
@ -648,32 +649,27 @@ fn parseBodyFromRequest(comptime T: type, content_type: ?[]const u8, reader: any
// Use json by default for now for testing purposes
const eff_type = content_type orelse "application/json";
const parser_type = matchContentType(eff_type);
const buf = try reader.readAllAlloc(alloc, 1 << 16);
defer alloc.free(buf);
switch (parser_type) {
.octet_stream, .json => {
const buf = try reader.readAllAlloc(alloc, 1 << 16);
defer alloc.free(buf);
const body = try json_utils.parse(T, buf, alloc);
defer json_utils.parseFree(body, alloc);
return try util.deepClone(alloc, body);
},
.url_encoded => return urlencode.parse(alloc, T, buf) catch |err| switch (err) {
//error.NoQuery => error.NoBody,
else => err,
},
.multipart_formdata => {
const param_string = std.mem.split(u8, eff_type, ";").rest();
const params = urlencode.parse(alloc, struct {
boundary: []const u8,
}, param_string) catch |err| return switch (err) {
//error.NoQuery => error.MissingBoundary,
.url_encoded => {
const buf = try reader.readAllAlloc(alloc, 1 << 16);
defer alloc.free(buf);
return urlencode.parse(alloc, T, buf) catch |err| switch (err) {
//error.NoQuery => error.NoBody,
else => err,
};
defer util.deepFree(alloc, params);
unreachable;
//try @import("./multipart.zig").parseFormData(params.boundary, reader, alloc);
},
.multipart_formdata => {
const boundary = fields.getParam(eff_type, "boundary") orelse return error.MissingBoundary;
return try @import("./multipart.zig").parseFormData(T, boundary, reader, alloc);
},
else => return error.UnsupportedMediaType,
}

View File

@ -1,6 +1,6 @@
const std = @import("std");
const util = @import("util");
const http = @import("./lib.zig");
const fields = @import("./fields.zig");
const max_boundary = 70;
const read_ahead = max_boundary + 4;
@ -37,11 +37,11 @@ pub fn MultipartStream(comptime ReaderType: type) type {
pub const Part = struct {
base: ?*Multipart,
fields: http.Fields,
fields: fields.Fields,
pub fn open(base: *Multipart, alloc: std.mem.Allocator) !Part {
var fields = try @import("./request/parser.zig").parseHeaders(alloc, base.stream.reader());
return .{ .base = base, .fields = fields };
var parsed_fields = try @import("./request/parser.zig").parseHeaders(alloc, base.stream.reader());
return .{ .base = base, .fields = parsed_fields };
}
pub fn reader(self: *Part) PartReader {
@ -101,61 +101,26 @@ pub fn openMultipart(boundary: []const u8, reader: anytype) !MultipartStream(@Ty
return stream;
}
const ParamIter = struct {
str: []const u8,
index: usize = 0,
const Param = struct {
name: []const u8,
value: []const u8,
};
pub fn from(str: []const u8) ParamIter {
return .{ .str = str, .index = std.mem.indexOfScalar(u8, str, ';') orelse str.len };
}
pub fn fieldValue(self: *ParamIter) []const u8 {
return std.mem.sliceTo(self.str, ';');
}
pub fn next(self: *ParamIter) ?Param {
if (self.index >= self.str.len) return null;
const start = self.index + 1;
const new_start = std.mem.indexOfScalarPos(u8, self.str, start, ';') orelse self.str.len;
self.index = new_start;
const param = std.mem.trim(u8, self.str[start..new_start], " \t");
var split = std.mem.split(u8, param, "=");
const name = split.first();
const value = std.mem.trimLeft(u8, split.rest(), " \t");
// TODO: handle quoted values
// TODO: handle parse errors
return Param{
.name = name,
.value = value,
};
}
};
pub fn parseFormData(comptime T: type, boundary: []const u8, reader: anytype, alloc: std.mem.Allocator) !T {
var multipart = try openMultipart(boundary, reader);
var ds = util.Deserializer(T){};
defer {
var iter = ds.iterator();
while (iter.next()) |pair| {
alloc.free(pair.value);
}
}
while (true) {
var part = (try multipart.next(alloc)) orelse break;
defer part.close();
const disposition = part.fields.get("Content-Disposition") orelse return error.InvalidForm;
var iter = ParamIter.from(disposition);
if (!std.ascii.eqlIgnoreCase("form-data", iter.fieldValue())) return error.InvalidForm;
const name = while (iter.next()) |param| {
if (!std.ascii.eqlIgnoreCase("name", param.name)) @panic("Not implemented");
break param.value;
} else return error.InvalidForm;
const name = fields.getParam(disposition, "name") orelse return error.InvalidForm;
const value = try part.reader().readAllAlloc(alloc, 1 << 32);
errdefer alloc.free(value);
try ds.setSerializedField(name, value);
}
@ -164,34 +129,79 @@ pub fn parseFormData(comptime T: type, boundary: []const u8, reader: anytype, al
// TODO: Fix these tests
test "MultipartStream" {
const body = util.comptimeToCrlf(
\\--abcd
\\Content-Disposition: form-data; name=first; charset=utf8
\\
\\content
\\--abcd
\\content-Disposition: form-data; name=second
\\
\\no content
\\--abcd
\\content-disposition: form-data; name=third
\\
\\
\\--abcd--
\\
const ExpectedPart = struct {
disposition: []const u8,
value: []const u8,
};
const testCase = struct {
fn case(comptime body: []const u8, boundary: []const u8, expected_parts: []const ExpectedPart) !void {
var src = std.io.StreamSource{
.const_buffer = std.io.fixedBufferStream(body),
};
var stream = try openMultipart(boundary, src.reader());
for (expected_parts) |expected| {
var part = try stream.next(std.testing.allocator) orelse return error.TestExpectedEqual;
defer part.close();
const dispo = part.fields.get("Content-Disposition") orelse return error.TestExpectedEqual;
try std.testing.expectEqualStrings(expected.disposition, dispo);
var buf: [128]u8 = undefined;
const count = try part.reader().read(&buf);
try std.testing.expectEqualStrings(expected.value, buf[0..count]);
}
try std.testing.expect(try stream.next(std.testing.allocator) == null);
}
}.case;
try testCase("--abc--\r\n", "abc", &.{});
try testCase(
util.comptimeToCrlf(
\\------abcd
\\Content-Disposition: form-data; name=first; charset=utf8
\\
\\content
\\------abcd
\\content-Disposition: form-data; name=second
\\
\\no content
\\------abcd
\\content-disposition: form-data; name=third
\\
\\
\\------abcd--
\\
),
"----abcd",
&.{
.{ .disposition = "form-data; name=first; charset=utf8", .value = "content" },
.{ .disposition = "form-data; name=second", .value = "no content" },
.{ .disposition = "form-data; name=third", .value = "" },
},
);
var src = std.io.StreamSource{ .const_buffer = std.io.fixedBufferStream(body) };
if (true) return error.SkipZigTest;
var stream = try openMultipart("abcd", src.reader());
while (try stream.next(std.testing.allocator)) |p| {
var part = p;
defer part.close();
std.debug.print("\n{?s}\n", .{part.fields.get("content-disposition")});
var buf: [64]u8 = undefined;
std.debug.print("\"{s}\"\n", .{buf[0..try part.reader().readAll(&buf)]});
}
try testCase(
util.comptimeToCrlf(
\\--xyz
\\Content-Disposition: uhh
\\
\\xyz
\\--xyz
\\Content-disposition: ok
\\
\\ --xyz
\\--xyz--
\\
),
"xyz",
&.{
.{ .disposition = "uhh", .value = "xyz" },
.{ .disposition = "ok", .value = " --xyz" },
},
);
}
test "parseFormData" {
@ -203,10 +213,10 @@ test "parseFormData" {
\\--abcd--
\\
);
if (true) return error.SkipZigTest;
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);
std.debug.print("\n\n\n\"{any}\"\n\n\n", .{val});
_ = val;
}