Start work on multipart form parser
This commit is contained in:
parent
b99a0095d4
commit
938ee61477
3 changed files with 182 additions and 11 deletions
|
@ -587,13 +587,15 @@ const BaseContentType = enum {
|
||||||
json,
|
json,
|
||||||
url_encoded,
|
url_encoded,
|
||||||
octet_stream,
|
octet_stream,
|
||||||
|
multipart_formdata,
|
||||||
|
|
||||||
other,
|
other,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn parseBodyFromRequest(comptime T: type, content_type: ?[]const u8, reader: anytype, alloc: std.mem.Allocator) !T {
|
fn parseBodyFromRequest(comptime T: type, content_type: ?[]const u8, reader: anytype, alloc: std.mem.Allocator) !T {
|
||||||
// Use json by default for now for testing purposes
|
// Use json by default for now for testing purposes
|
||||||
const parser_type = matchContentType(content_type) orelse .json;
|
const eff_type = content_type orelse "application/json";
|
||||||
|
const parser_type = matchContentType(eff_type);
|
||||||
const buf = try reader.readAllAlloc(alloc, 1 << 16);
|
const buf = try reader.readAllAlloc(alloc, 1 << 16);
|
||||||
defer alloc.free(buf);
|
defer alloc.free(buf);
|
||||||
|
|
||||||
|
@ -608,21 +610,32 @@ fn parseBodyFromRequest(comptime T: type, content_type: ?[]const u8, reader: any
|
||||||
error.NoQuery => error.NoBody,
|
error.NoQuery => error.NoBody,
|
||||||
else => err,
|
else => err,
|
||||||
},
|
},
|
||||||
|
.multipart_formdata => {
|
||||||
|
const param_string = std.mem.split(u8, eff_type, ";").rest();
|
||||||
|
const params = query_utils.parseQuery(alloc, struct {
|
||||||
|
boundary: []const u8,
|
||||||
|
}, param_string) catch |err| return switch (err) {
|
||||||
|
error.NoQuery => error.MissingBoundary,
|
||||||
|
else => err,
|
||||||
|
};
|
||||||
|
defer util.deepFree(alloc, params);
|
||||||
|
|
||||||
|
try @import("./multipart.zig").parseFormData(params.boundary, reader, alloc);
|
||||||
|
},
|
||||||
else => return error.UnsupportedMediaType,
|
else => return error.UnsupportedMediaType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// figure out what base parser to use
|
// figure out what base parser to use
|
||||||
fn matchContentType(hdr: ?[]const u8) ?BaseContentType {
|
fn matchContentType(hdr: []const u8) BaseContentType {
|
||||||
if (hdr) |h| {
|
const trimmed = std.mem.sliceTo(hdr, ';');
|
||||||
if (std.ascii.eqlIgnoreCase(h, "application/x-www-form-urlencoded")) return .url_encoded;
|
if (std.ascii.eqlIgnoreCase(trimmed, "application/x-www-form-urlencoded")) return .url_encoded;
|
||||||
if (std.ascii.eqlIgnoreCase(h, "application/json")) return .json;
|
if (std.ascii.eqlIgnoreCase(trimmed, "application/json")) return .json;
|
||||||
if (std.ascii.eqlIgnoreCase(h, "application/octet-stream")) return .octet_stream;
|
if (std.ascii.endsWithIgnoreCase(trimmed, "+json")) return .json;
|
||||||
|
if (std.ascii.eqlIgnoreCase(trimmed, "application/octet-stream")) return .octet_stream;
|
||||||
|
if (std.ascii.eqlIgnoreCase(trimmed, "multipart/form-data")) return .multipart_formdata;
|
||||||
|
|
||||||
return .other;
|
return .other;
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a set of body arguments from the request body based on the request's Content-Type
|
/// Parses a set of body arguments from the request body based on the request's Content-Type
|
||||||
|
@ -674,6 +687,10 @@ test "parseBodyFromRequest" {
|
||||||
};
|
};
|
||||||
try testCase("application/json", "{\"id\": 3}", Struct{ .id = 3 });
|
try testCase("application/json", "{\"id\": 3}", Struct{ .id = 3 });
|
||||||
try testCase("application/x-www-form-urlencoded", "id=3", Struct{ .id = 3 });
|
try testCase("application/x-www-form-urlencoded", "id=3", Struct{ .id = 3 });
|
||||||
|
|
||||||
|
try testCase("multipart/form-data; ",
|
||||||
|
\\
|
||||||
|
, Struct{ .id = 3 });
|
||||||
}
|
}
|
||||||
|
|
||||||
test "parseBody" {
|
test "parseBody" {
|
||||||
|
|
154
src/http/multipart.zig
Normal file
154
src/http/multipart.zig
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const max_boundary = 70;
|
||||||
|
|
||||||
|
const FormField = struct {
|
||||||
|
//name: []const u8,
|
||||||
|
//disposition: []const u8,
|
||||||
|
//filename: ?[]const u8,
|
||||||
|
//charset: ?[]const u8,
|
||||||
|
value: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormFieldResult = struct {
|
||||||
|
field: FormField,
|
||||||
|
more: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn isFinalPart(peek_stream: anytype) !bool {
|
||||||
|
const reader = peek_stream.reader();
|
||||||
|
var buf: [2]u8 = undefined;
|
||||||
|
const end = try reader.readAll(&buf);
|
||||||
|
const end_line = buf[0..end];
|
||||||
|
const terminal = std.mem.eql(u8, end_line, "--");
|
||||||
|
if (!terminal) try peek_stream.putBack(end_line);
|
||||||
|
|
||||||
|
// Skip whitespace
|
||||||
|
while (true) {
|
||||||
|
const b = reader.readByte() catch |err| switch (err) {
|
||||||
|
error.EndOfStream => {
|
||||||
|
if (terminal) break else return error.InvalidMultipartBoundary;
|
||||||
|
},
|
||||||
|
else => return err,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (std.mem.indexOfScalar(u8, " \r\n", b) == null) {
|
||||||
|
try peek_stream.putBackByte(b);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return terminal;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseFormField(boundary: []const u8, peek_stream: anytype, alloc: std.mem.Allocator) !FormFieldResult {
|
||||||
|
const reader = peek_stream.reader();
|
||||||
|
|
||||||
|
// TODO: refactor
|
||||||
|
var headers = try @import("./request/parser.zig").parseHeaders(alloc, reader);
|
||||||
|
defer headers.deinit();
|
||||||
|
|
||||||
|
std.debug.print("disposition: {?s}\n", .{headers.get("Content-Disposition")});
|
||||||
|
|
||||||
|
var value = std.ArrayList(u8).init(alloc);
|
||||||
|
errdefer value.deinit();
|
||||||
|
|
||||||
|
line_loop: while (true) {
|
||||||
|
// parse crlf--
|
||||||
|
var buf: [4]u8 = undefined;
|
||||||
|
try reader.readNoEof(&buf);
|
||||||
|
if (!std.mem.eql(u8, &buf, "\r\n--")) {
|
||||||
|
try value.append(buf[0]);
|
||||||
|
try peek_stream.putBack(buf[1..]);
|
||||||
|
var ch = try reader.readByte();
|
||||||
|
while (ch != '\r') : (ch = try reader.readByte()) try value.append(ch);
|
||||||
|
|
||||||
|
try peek_stream.putBackByte(ch);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (boundary) |ch, i| {
|
||||||
|
const b = try reader.readByte();
|
||||||
|
|
||||||
|
if (b != ch) {
|
||||||
|
try value.appendSlice("\r\n--");
|
||||||
|
try value.appendSlice(boundary[0 .. i + 1]);
|
||||||
|
continue :line_loop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boundary parsed. See if its a terminal or not
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const terminal = try isFinalPart(peek_stream);
|
||||||
|
|
||||||
|
return FormFieldResult{
|
||||||
|
.field = .{ .value = value.toOwnedSlice() },
|
||||||
|
.more = !terminal,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parseFormData(boundary: []const u8, reader: anytype, alloc: std.mem.Allocator) !void {
|
||||||
|
if (boundary.len > max_boundary) return error.BoundaryTooLarge;
|
||||||
|
|
||||||
|
var stream = std.io.peekStream(72, reader);
|
||||||
|
{
|
||||||
|
var buf: [72]u8 = undefined;
|
||||||
|
const count = try stream.reader().readAll(buf[0 .. boundary.len + 2]);
|
||||||
|
var line = buf[0..count];
|
||||||
|
if (line.len != boundary.len + 2) return error.InvalidMultipartBoundary;
|
||||||
|
if (!std.mem.startsWith(u8, line, "--")) return error.InvalidMultipartBoundary;
|
||||||
|
if (!std.mem.endsWith(u8, line, boundary)) return error.InvalidMultipartBoundary;
|
||||||
|
|
||||||
|
if (try isFinalPart(&stream)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const field = try parseFormField(boundary, &stream, alloc);
|
||||||
|
alloc.free(field.field.value);
|
||||||
|
|
||||||
|
if (!field.more) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn toCrlf(comptime str: []const u8) []const u8 {
|
||||||
|
comptime {
|
||||||
|
var buf: [str.len * 2]u8 = undefined;
|
||||||
|
|
||||||
|
@setEvalBranchQuota(@intCast(u32, str.len * 2)); // TODO: why does this need to be *2
|
||||||
|
|
||||||
|
var buf_len: usize = 0;
|
||||||
|
for (str) |ch| {
|
||||||
|
if (ch == '\n') {
|
||||||
|
buf[buf_len] = '\r';
|
||||||
|
buf_len += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
buf[buf_len] = ch;
|
||||||
|
buf_len += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf[0..buf_len];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseFormData" {
|
||||||
|
const body = toCrlf(
|
||||||
|
\\--abcd
|
||||||
|
\\Content-Disposition: form-data; name=first
|
||||||
|
\\
|
||||||
|
\\content
|
||||||
|
\\--abcd
|
||||||
|
\\content-Disposition: form-data; name=second
|
||||||
|
\\
|
||||||
|
\\no content
|
||||||
|
\\--abcd
|
||||||
|
\\content-disposition: form-data; name=third
|
||||||
|
\\
|
||||||
|
\\
|
||||||
|
\\--abcd--
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
var stream = std.io.StreamSource{ .const_buffer = std.io.fixedBufferStream(body) };
|
||||||
|
try parseFormData("abcd", stream.reader(), std.testing.allocator);
|
||||||
|
}
|
|
@ -93,7 +93,7 @@ fn parseProto(reader: anytype) !http.Protocol {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parseHeaders(allocator: std.mem.Allocator, reader: anytype) !Fields {
|
pub fn parseHeaders(allocator: std.mem.Allocator, reader: anytype) !Fields {
|
||||||
var headers = Fields.init(allocator);
|
var headers = Fields.init(allocator);
|
||||||
|
|
||||||
var buf: [4096]u8 = undefined;
|
var buf: [4096]u8 = undefined;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue