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

View file

@ -1,6 +1,6 @@
const std = @import("std"); const std = @import("std");
const util = @import("util"); const util = @import("util");
const http = @import("./lib.zig"); const fields = @import("./fields.zig");
const max_boundary = 70; const max_boundary = 70;
const read_ahead = max_boundary + 4; const read_ahead = max_boundary + 4;
@ -37,11 +37,11 @@ pub fn MultipartStream(comptime ReaderType: type) type {
pub const Part = struct { pub const Part = struct {
base: ?*Multipart, base: ?*Multipart,
fields: http.Fields, fields: fields.Fields,
pub fn open(base: *Multipart, alloc: std.mem.Allocator) !Part { pub fn open(base: *Multipart, alloc: std.mem.Allocator) !Part {
var fields = try @import("./request/parser.zig").parseHeaders(alloc, base.stream.reader()); var parsed_fields = try @import("./request/parser.zig").parseHeaders(alloc, base.stream.reader());
return .{ .base = base, .fields = fields }; return .{ .base = base, .fields = parsed_fields };
} }
pub fn reader(self: *Part) PartReader { pub fn reader(self: *Part) PartReader {
@ -101,61 +101,26 @@ pub fn openMultipart(boundary: []const u8, reader: anytype) !MultipartStream(@Ty
return stream; 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 { pub fn parseFormData(comptime T: type, boundary: []const u8, reader: anytype, alloc: std.mem.Allocator) !T {
var multipart = try openMultipart(boundary, reader); var multipart = try openMultipart(boundary, reader);
var ds = util.Deserializer(T){}; var ds = util.Deserializer(T){};
defer {
var iter = ds.iterator();
while (iter.next()) |pair| {
alloc.free(pair.value);
}
}
while (true) { while (true) {
var part = (try multipart.next(alloc)) orelse break; var part = (try multipart.next(alloc)) orelse break;
defer part.close(); defer part.close();
const disposition = part.fields.get("Content-Disposition") orelse return error.InvalidForm; 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 = fields.getParam(disposition, "name") orelse 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 value = try part.reader().readAllAlloc(alloc, 1 << 32); const value = try part.reader().readAllAlloc(alloc, 1 << 32);
errdefer alloc.free(value);
try ds.setSerializedField(name, 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 // TODO: Fix these tests
test "MultipartStream" { test "MultipartStream" {
const body = util.comptimeToCrlf( const ExpectedPart = struct {
\\--abcd disposition: []const u8,
\\Content-Disposition: form-data; name=first; charset=utf8 value: []const u8,
\\ };
\\content const testCase = struct {
\\--abcd fn case(comptime body: []const u8, boundary: []const u8, expected_parts: []const ExpectedPart) !void {
\\content-Disposition: form-data; name=second var src = std.io.StreamSource{
\\ .const_buffer = std.io.fixedBufferStream(body),
\\no content };
\\--abcd
\\content-disposition: form-data; name=third var stream = try openMultipart(boundary, src.reader());
\\
\\ for (expected_parts) |expected| {
\\--abcd-- 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) }; try testCase(
util.comptimeToCrlf(
if (true) return error.SkipZigTest; \\--xyz
var stream = try openMultipart("abcd", src.reader()); \\Content-Disposition: uhh
while (try stream.next(std.testing.allocator)) |p| { \\
var part = p; \\xyz
defer part.close(); \\--xyz
std.debug.print("\n{?s}\n", .{part.fields.get("content-disposition")}); \\Content-disposition: ok
var buf: [64]u8 = undefined; \\
std.debug.print("\"{s}\"\n", .{buf[0..try part.reader().readAll(&buf)]}); \\ --xyz
} \\--xyz--
\\
),
"xyz",
&.{
.{ .disposition = "uhh", .value = "xyz" },
.{ .disposition = "ok", .value = " --xyz" },
},
);
} }
test "parseFormData" { test "parseFormData" {
@ -203,10 +213,10 @@ test "parseFormData" {
\\--abcd-- \\--abcd--
\\ \\
); );
if (true) return error.SkipZigTest; 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);
std.debug.print("\n\n\n\"{any}\"\n\n\n", .{val}); _ = val;
} }