207 lines
5.8 KiB
Zig
207 lines
5.8 KiB
Zig
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);
|
|
}
|