const std = @import("std"); const util = @import("util"); const max_boundary = 70; const FormFieldResult = struct { 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; 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, " \t\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(); 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); const disposition = headers.get("Content-Disposition") orelse return error.NoDisposition; return FormFieldResult{ .value = value.toOwnedSlice(), .params = try parseParams(alloc, FormDataParams, disposition), .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); defer util.deepFree(alloc, field); std.debug.print("{any}\n", .{field}); 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; charset=utf8 \\ \\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); }