diff --git a/build.zig b/build.zig index dafaf08..c686472 100644 --- a/build.zig +++ b/build.zig @@ -18,6 +18,10 @@ pub fn build(b: *std.build.Builder) void { deps.addAllTo(exe); exe.install(); + const exe_test = b.addTest("src/main.zig"); + exe_test.setTarget(target); + deps.addAllTo(exe_test); + const run_cmd = exe.run(); run_cmd.step.dependOn(b.getInstallStep()); if (b.args) |args| { @@ -26,4 +30,7 @@ pub fn build(b: *std.build.Builder) void { const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); + + const test_step = b.step("test", "Test stuff"); + test_step.dependOn(&exe_test.step); } diff --git a/src/main.zig b/src/main.zig index ac19d7a..bf19559 100644 --- a/src/main.zig +++ b/src/main.zig @@ -3,6 +3,8 @@ const http = @import("apple_pie"); const hzzp = @import("hzzp"); const mimetypes = @import("mimetypes"); +const fmt = std.fmt; + const images_dir_path = "./images"; var registry: mimetypes.Registry = undefined; @@ -58,14 +60,54 @@ fn generateImageId(buffer: []u8) []const u8 { const StreamT = std.io.FixedBufferStream([]const u8); const ContentDisposition = struct { + allocator: *std.mem.Allocator, name: []const u8, filename: []const u8, + + const Self = @This(); + + pub fn deinit(self: *Self) void { + self.allocator.free(self.name); + self.allocator.free(self.filename); + } + + pub fn format(self: Self, comptime f: []const u8, options: fmt.FormatOptions, writer: anytype) !void { + if (f.len != 0) { + @compileError("Unknown format character: '" ++ f ++ "'"); + } + + return fmt.format( + writer, + "Disposition{{.name='{s}', .filename='{s}'}}", + .{ self.name, self.filename }, + ); + } }; const Part = struct { + allocator: *std.mem.Allocator, disposition: ContentDisposition, content_type: []const u8, body: []const u8, + + const Self = @This(); + + pub fn deinit(self: *Self) void { + self.disposition.deinit(); + self.allocator.free(self.content_type); + } + + pub fn format(self: Self, comptime f: []const u8, options: fmt.FormatOptions, writer: anytype) !void { + if (f.len != 0) { + @compileError("Unknown format character: '" ++ f ++ "'"); + } + + return fmt.format( + writer, + "Part{{.content_type='{s}', .disposition={}, .body='{s}'}}", + .{ self.content_type, self.disposition, self.body }, + ); + } }; const Multipart = struct { @@ -103,7 +145,7 @@ const Multipart = struct { }; } - pub fn next(self: *Self, hzzp_buffer: []u8) !?Part { + pub fn next(self: *Self, hzzp_buffer: []u8, allocator: *std.mem.Allocator) !?Part { var reader = self.stream.reader(); // first self.boundary.len+2 bytes MUST be boundary + \r + \n var boundary_buffer: [512]u8 = undefined; @@ -162,12 +204,13 @@ const Multipart = struct { } content_disposition = ContentDisposition{ - .name = dispo_name, - .filename = dispo_filename, + .allocator = allocator, + .name = try std.mem.dupe(allocator, u8, dispo_name), + .filename = try std.mem.dupe(allocator, u8, dispo_filename), }; std.log.debug("got content disposition for part! {}", .{content_disposition}); } else if (std.mem.eql(u8, header.name, "Content-Type")) { - content_type = header.value; + content_type = try std.mem.dupe(allocator, u8, header.value); std.log.debug("got content type for part! {s}", .{content_type}); } }, @@ -193,7 +236,10 @@ const Multipart = struct { const boundary_end_marker = try std.fmt.bufPrint(&end_boundary_buf, "{s}--\r\n", .{self.boundary}); const actual_body = std.mem.trim(u8, remaining_body, boundary_end_marker); + std.debug.warn("ctype out of this: '{s}'", .{content_type.?}); + return Part{ + .allocator = allocator, .disposition = content_disposition.?, .content_type = content_type.?, .body = actual_body, @@ -268,3 +314,43 @@ fn fetchFile(response: *http.Response, request: http.Request, filename: []const try response.writer().writeAll(&file_write_buffer); } } + +pub const log_level: std.log.Level = .debug; +test "multipart" { + const body = + "--1234\r\n" ++ + "Content-Type: text/plain\r\n" ++ + "Content-Disposition: form-data; name=file1; filename=ab.txt\r\n" ++ + "\r\n" ++ + "Hello!\n" ++ + "--1234\r\n" ++ + "Content-Type: application/json\r\n" ++ + // TODO: add 'content-type' support to content-disposition as well + "Content-Disposition: form-data; name=file2; filename=data.json\r\n" ++ + "\r\n" ++ + "{\"status\": \"OK\"}\n" ++ + "--1234--\r\n"; + + var buf: [512]u8 = undefined; + var multipart = try Multipart.init( + body, + "multipart/form-data; boundary=1234", + &buf, + ); + + var hzzp_buffer: [1024]u8 = undefined; + var part1 = (try multipart.next(&hzzp_buffer, std.testing.allocator)).?; + defer part1.deinit(); + + std.debug.warn( + "\npart={}\n", + .{part1}, + ); + + std.testing.expectEqualSlices(u8, "text/plain", part1.content_type); + std.testing.expectEqualSlices(u8, "file1", part1.disposition.name); + std.testing.expectEqualSlices(u8, "ab.txt", part1.disposition.filename); + std.testing.expectEqualSlices(u8, "Hello!", part1.body); + + // var part2 = (try multipart.next(&hzzp_buffer)).?; +}