Multipart/form-data
This commit is contained in:
parent
0b13f210c7
commit
6e56775d61
2 changed files with 106 additions and 100 deletions
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue