fediglam/src/http/multipart.zig

223 lines
7.4 KiB
Zig
Raw Normal View History

2022-11-27 13:43:06 +00:00
const std = @import("std");
2022-11-27 14:11:01 +00:00
const util = @import("util");
2022-12-03 05:49:27 +00:00
const fields = @import("./fields.zig");
2022-11-27 13:43:06 +00:00
const max_boundary = 70;
2022-12-01 09:56:17 +00:00
const read_ahead = max_boundary + 4;
pub fn MultipartStream(comptime ReaderType: type) type {
return struct {
const Multipart = @This();
pub const PartReader = std.io.Reader(*Part, ReaderType.Error, Part.read);
stream: std.io.PeekStream(.{ .Static = read_ahead }, ReaderType),
boundary: []const u8,
pub fn next(self: *Multipart, alloc: std.mem.Allocator) !?Part {
const reader = self.stream.reader();
while (true) {
try reader.skipUntilDelimiterOrEof('\r');
var line_buf: [read_ahead]u8 = undefined;
const len = try reader.readAll(line_buf[0 .. self.boundary.len + 3]);
const line = line_buf[0..len];
if (line.len == 0) return null;
if (std.mem.startsWith(u8, line, "\n--") and std.mem.endsWith(u8, line, self.boundary)) {
// match, check for end thing
var more_buf: [2]u8 = undefined;
if (try reader.readAll(&more_buf) != 2) return error.EndOfStream;
const more = !(more_buf[0] == '-' and more_buf[1] == '-');
try self.stream.putBack(&more_buf);
try reader.skipUntilDelimiterOrEof('\n');
if (more) return try Part.open(self, alloc) else return null;
}
}
}
2022-11-27 13:43:06 +00:00
2022-12-01 09:56:17 +00:00
pub const Part = struct {
base: ?*Multipart,
2022-12-03 05:49:27 +00:00
fields: fields.Fields,
2022-11-27 13:43:06 +00:00
2022-12-01 09:56:17 +00:00
pub fn open(base: *Multipart, alloc: std.mem.Allocator) !Part {
2022-12-03 05:49:27 +00:00
var parsed_fields = try @import("./request/parser.zig").parseHeaders(alloc, base.stream.reader());
return .{ .base = base, .fields = parsed_fields };
2022-12-01 09:56:17 +00:00
}
pub fn reader(self: *Part) PartReader {
return .{ .context = self };
}
pub fn close(self: *Part) void {
self.fields.deinit();
}
pub fn read(self: *Part, buf: []u8) ReaderType.Error!usize {
const base = self.base orelse return 0;
const r = base.stream.reader();
var count: usize = 0;
while (count < buf.len) {
const byte = r.readByte() catch |err| switch (err) {
error.EndOfStream => {
self.base = null;
return count;
},
else => |e| return e,
};
buf[count] = byte;
count += 1;
if (byte != '\r') continue;
var line_buf: [read_ahead]u8 = undefined;
const line = line_buf[0..try r.readAll(line_buf[0 .. base.boundary.len + 3])];
if (!std.mem.startsWith(u8, line, "\n--") or !std.mem.endsWith(u8, line, base.boundary)) {
base.stream.putBack(line) catch unreachable;
continue;
} else {
base.stream.putBack(line) catch unreachable;
base.stream.putBackByte('\r') catch unreachable;
self.base = null;
return count - 1;
}
}
return count;
}
};
};
}
pub fn openMultipart(boundary: []const u8, reader: anytype) !MultipartStream(@TypeOf(reader)) {
if (boundary.len > max_boundary) return error.BoundaryTooLarge;
var stream = .{
.stream = std.io.peekStream(read_ahead, reader),
.boundary = boundary,
};
stream.stream.putBack("\r\n") catch unreachable;
return stream;
}
2022-11-27 14:24:41 +00:00
2022-11-28 06:33:05 +00:00
pub fn parseFormData(comptime T: type, boundary: []const u8, reader: anytype, alloc: std.mem.Allocator) !T {
2022-12-01 09:56:17 +00:00
var multipart = try openMultipart(boundary, reader);
2022-11-27 13:43:06 +00:00
2022-12-01 09:56:17 +00:00
var ds = util.Deserializer(T){};
2022-12-03 05:49:27 +00:00
defer {
var iter = ds.iterator();
while (iter.next()) |pair| {
alloc.free(pair.value);
}
}
2022-11-27 13:43:06 +00:00
while (true) {
2022-12-01 09:56:17 +00:00
var part = (try multipart.next(alloc)) orelse break;
defer part.close();
const disposition = part.fields.get("Content-Disposition") orelse return error.InvalidForm;
2022-12-03 05:49:27 +00:00
const name = fields.getParam(disposition, "name") orelse return error.InvalidForm;
2022-12-01 09:56:17 +00:00
const value = try part.reader().readAllAlloc(alloc, 1 << 32);
2022-12-03 05:49:27 +00:00
errdefer alloc.free(value);
2022-12-01 09:56:17 +00:00
try ds.setSerializedField(name, value);
2022-11-27 14:24:41 +00:00
}
2022-12-01 09:56:17 +00:00
return try ds.finish(alloc);
2022-11-27 14:24:41 +00:00
}
2022-12-01 09:56:17 +00:00
// TODO: Fix these tests
test "MultipartStream" {
2022-12-03 05:49:27 +00:00
const ExpectedPart = struct {
disposition: []const u8,
value: []const u8,
};
const testCase = struct {
fn case(comptime body: []const u8, boundary: []const u8, expected_parts: []const ExpectedPart) !void {
var src = std.io.StreamSource{
.const_buffer = std.io.fixedBufferStream(body),
};
2022-11-27 14:11:01 +00:00
2022-12-03 05:49:27 +00:00
var stream = try openMultipart(boundary, src.reader());
2022-12-01 09:56:17 +00:00
2022-12-03 05:49:27 +00:00
for (expected_parts) |expected| {
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 = "" },
},
);
try testCase(
util.comptimeToCrlf(
\\--xyz
\\Content-Disposition: uhh
\\
\\xyz
\\--xyz
\\Content-disposition: ok
\\
\\ --xyz
\\--xyz--
\\
),
"xyz",
&.{
.{ .disposition = "uhh", .value = "xyz" },
.{ .disposition = "ok", .value = " --xyz" },
},
);
2022-12-01 09:56:17 +00:00
}
test "parseFormData" {
2022-12-02 03:45:09 +00:00
const body = util.comptimeToCrlf(
2022-12-01 09:56:17 +00:00
\\--abcd
\\Content-Disposition: form-data; name=foo
\\
\\content
\\--abcd--
\\
);
2022-12-03 05:49:27 +00:00
if (true) return error.SkipZigTest; // TODO
2022-12-01 09:56:17 +00:00
var src = std.io.StreamSource{ .const_buffer = std.io.fixedBufferStream(body) };
const val = try parseFormData(struct {
foo: []const u8,
}, "abcd", src.reader(), std.testing.allocator);
2022-12-03 05:49:27 +00:00
_ = val;
2022-11-27 13:43:06 +00:00
}