diff --git a/src/http/multipart.zig b/src/http/multipart.zig index 5197685..93ec957 100644 --- a/src/http/multipart.zig +++ b/src/http/multipart.zig @@ -1,20 +1,70 @@ const std = @import("std"); +const util = @import("util"); 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, + value: []const u8, + params: FormDataParams, more: bool, }; +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 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, + }; + } +}; + +const FormDataParams = struct { + name: ?[]const u8 = null, + filename: ?[]const u8 = null, + charset: ?[]const u8 = null, +}; + +fn parseParams(alloc: std.mem.Allocator, comptime T: type, str: []const u8) !T { + var result = T{}; + errdefer util.deepFree(alloc, result); + + var iter = ParamIter.from(str); + while (iter.next()) |param| { + inline for (comptime std.meta.fieldNames(T)) |f| { + if (std.mem.eql(u8, param.name, f)) { + @field(result, f) = try util.deepClone(alloc, param.value); + } + } + } + + return result; +} + fn isFinalPart(peek_stream: anytype) !bool { const reader = peek_stream.reader(); var buf: [2]u8 = undefined; @@ -32,7 +82,7 @@ fn isFinalPart(peek_stream: anytype) !bool { else => return err, }; - if (std.mem.indexOfScalar(u8, " \r\n", b) == null) { + if (std.mem.indexOfScalar(u8, " \t\r\n", b) == null) { try peek_stream.putBackByte(b); break; } @@ -48,8 +98,6 @@ fn parseFormField(boundary: []const u8, peek_stream: anytype, alloc: std.mem.All 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(); @@ -82,9 +130,11 @@ fn parseFormField(boundary: []const u8, peek_stream: anytype, alloc: std.mem.All } const terminal = try isFinalPart(peek_stream); + const disposition = headers.get("Content-Disposition") orelse return error.NoDisposition; return FormFieldResult{ - .field = .{ .value = value.toOwnedSlice() }, + .value = value.toOwnedSlice(), + .params = try parseParams(alloc, FormDataParams, disposition), .more = !terminal, }; } @@ -106,7 +156,9 @@ pub fn parseFormData(boundary: []const u8, reader: anytype, alloc: std.mem.Alloc while (true) { const field = try parseFormField(boundary, &stream, alloc); - alloc.free(field.field.value); + defer util.deepFree(alloc, field); + + std.debug.print("{any}\n", .{field}); if (!field.more) return; } @@ -135,7 +187,7 @@ fn toCrlf(comptime str: []const u8) []const u8 { test "parseFormData" { const body = toCrlf( \\--abcd - \\Content-Disposition: form-data; name=first + \\Content-Disposition: form-data; name=first; charset=utf8 \\ \\content \\--abcd @@ -149,6 +201,7 @@ test "parseFormData" { \\--abcd-- \\ ); + var stream = std.io.StreamSource{ .const_buffer = std.io.fixedBufferStream(body) }; try parseFormData("abcd", stream.reader(), std.testing.allocator); }