From 96a46a98c91fb070620c9b496335a7e0cd6c213b Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Sun, 27 Nov 2022 22:33:05 -0800 Subject: [PATCH] Multipart deserialization --- src/http/multipart.zig | 157 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 143 insertions(+), 14 deletions(-) diff --git a/src/http/multipart.zig b/src/http/multipart.zig index 1450153..5cd3585 100644 --- a/src/http/multipart.zig +++ b/src/http/multipart.zig @@ -4,8 +4,7 @@ const util = @import("util"); const max_boundary = 70; const FormFieldResult = struct { - value: []const u8, - params: FormDataParams, + field: FormField, more: bool, }; @@ -138,13 +137,15 @@ fn parseFormField(boundary: []const u8, peek_stream: anytype, alloc: std.mem.All const disposition = headers.get("Content-Disposition") orelse return error.NoDisposition; return FormFieldResult{ - .value = value.toOwnedSlice(), - .params = try parseParams(alloc, FormDataParams, disposition), + .field = .{ + .value = value.toOwnedSlice(), + .params = try parseParams(alloc, FormDataParams, disposition), + }, .more = !terminal, }; } -pub fn parseFormData(boundary: []const u8, reader: anytype, alloc: std.mem.Allocator) !void { +pub fn parseFormData(comptime T: type, boundary: []const u8, reader: anytype, alloc: std.mem.Allocator) !T { if (boundary.len > max_boundary) return error.BoundaryTooLarge; var stream = std.io.peekStream(72, reader); @@ -156,17 +157,137 @@ pub fn parseFormData(boundary: []const u8, reader: anytype, alloc: std.mem.Alloc if (!std.mem.startsWith(u8, line, "--")) return error.InvalidMultipartBoundary; if (!std.mem.endsWith(u8, line, boundary)) return error.InvalidMultipartBoundary; - if (try isFinalPart(&stream)) return; + if (try isFinalPart(&stream)) return error.NoForm; } + var fields = Intermediary(T){}; while (true) { - const field = try parseFormField(boundary, &stream, alloc); - defer util.deepFree(alloc, field); + const form_field = try parseFormField(boundary, &stream, alloc); - std.debug.print("{any}\n", .{field}); + inline for (std.meta.fields(Intermediary(T))) |field| { + if (std.ascii.eqlIgnoreCase(field.name[2..], form_field.field.params.name.?)) { + @field(fields, field.name) = form_field.field; + break; + } + } else { + std.log.debug("unknown form field {?s}", .{form_field.field.params.name}); + util.deepFree(alloc, form_field); + } - if (!field.more) return; + if (!form_field.more) break; } + + return (try parse(alloc, T, "", "", fields)).?; +} + +fn parse( + alloc: std.mem.Allocator, + comptime T: type, + comptime prefix: []const u8, + comptime name: []const u8, + fields: anytype, +) !?T { + if (comptime isScalar(T)) return try parseFormValue(alloc, T, @field(fields, prefix ++ "." ++ name)); + switch (@typeInfo(T)) { + .Union => |info| { + var result: ?T = null; + inline for (info.fields) |field| { + const F = field.field_type; + + const maybe_value = try parse(alloc, F, prefix, field.name, fields); + if (maybe_value) |value| { + if (result != null) return error.DuplicateUnionField; + + result = @unionInit(T, field.name, value); + } + } + std.log.debug("{any}", .{result}); + return result; + }, + + .Struct => |info| { + var result: T = undefined; + var fields_specified: usize = 0; + errdefer inline for (info.fields) |field, i| { + if (fields_specified < i) util.deepFree(alloc, @field(result, field.name)); + }; + + inline for (info.fields) |field| { + const F = field.field_type; + + var maybe_value: ?F = null; + if (try parse(alloc, F, prefix ++ "." ++ name, field.name, fields)) |v| { + maybe_value = v; + } else if (field.default_value) |default| { + if (comptime @sizeOf(F) != 0) { + maybe_value = try util.deepClone(alloc, @ptrCast(*const F, @alignCast(@alignOf(F), default)).*); + } else { + maybe_value = std.mem.zeroes(F); + } + } + + if (maybe_value) |v| { + fields_specified += 1; + @field(result, field.name) = v; + } + } + + if (fields_specified == 0) { + return null; + } else if (fields_specified != info.fields.len) { + std.log.debug("{} {s} {s}", .{ T, prefix, name }); + return error.PartiallySpecifiedStruct; + } else { + return result; + } + }, + + // Only applies to non-scalar optionals + .Optional => |info| return try parse(alloc, info.child, prefix, name, fields), + + else => @compileError("tmp"), + } +} + +fn recursiveFieldPaths(comptime T: type, comptime prefix: []const u8) []const []const u8 { + comptime { + if (std.meta.trait.is(.Optional)(T)) return recursiveFieldPaths(std.meta.Child(T), prefix); + + var fields: []const []const u8 = &.{}; + + for (std.meta.fields(T)) |f| { + const full_name = prefix ++ f.name; + + if (isScalar(f.field_type)) { + fields = fields ++ @as([]const []const u8, &.{full_name}); + } else { + const field_prefix = if (@typeInfo(f.field_type) == .Union) prefix else full_name ++ "."; + fields = fields ++ recursiveFieldPaths(f.field_type, field_prefix); + } + } + + return fields; + } +} + +fn Intermediary(comptime T: type) type { + const field_names = recursiveFieldPaths(T, ".."); + + var fields: [field_names.len]std.builtin.Type.StructField = undefined; + for (field_names) |name, i| fields[i] = .{ + .name = name, + .field_type = ?FormField, + .default_value = &@as(?FormField, null), + .is_comptime = false, + .alignment = @alignOf(?FormField), + }; + + return @Type(.{ .Struct = .{ + .layout = .Auto, + .fields = &fields, + .decls = &.{}, + .is_tuple = false, + } }); } const FormFile = struct { @@ -174,9 +295,8 @@ const FormFile = struct { data: []const u8, }; -fn parseFormValue(alloc: std.mem.Allocator, comptime T: type, field: FormField) !T { - const is_optional = std.meta.trait.is(.Optional)(T); - if ((comptime is_optional) and field.value.len == 0) return null; +fn parseFormValue(alloc: std.mem.Allocator, comptime T: type, f: ?FormField) !T { + const field = f orelse unreachable; if (comptime std.meta.trait.isZigString(T)) return field.value; @@ -275,6 +395,15 @@ test "parseFormData" { \\ ); + const T = struct { + first: []const u8, + second: []const u8, + third: []const u8, + }; var stream = std.io.StreamSource{ .const_buffer = std.io.fixedBufferStream(body) }; - try parseFormData("abcd", stream.reader(), std.testing.allocator); + const result = try parseFormData(T, "abcd", stream.reader(), std.testing.allocator); + std.debug.print("\nfirst: {s}\n\n", .{result.first}); + std.debug.print("\nsecond: {s}\n\n", .{result.second}); + std.debug.print("\nthird: {s}\n\n", .{result.third}); + std.debug.print("\n{any}\n\n", .{result}); }