edqx/wardrobe
Zig Multipart Form Data library
A lightweight, simple HTTP multipart/form-data
library for Zig.
Supports both reading and writing form data payloads.
To start writing form data, you need to create a boundary string for your application. The spec requires a certain number of bytes of entropy, so Wardrobe helps by giving you a helper struct for creating boundaries:
const boundary: wardrobe.Boundary = .entropy("MyApplicationBoundary", random);
random
is an interface instance of std.Random
, for example:
var prng = std.Random.DefaultPrng.init(@intCast(std.time.microTimestamp()));
const boundary: wardrobe.Boundary = .entropy("MyApplicationBoundary", prng.random());
If you want to pass in a boundary, you can also use wardrobe.Boundary.buffer
:
const boundary: wardrobe.Boundary = .buffer("----MyApplicationBoundaryRANDOMBYTES");
To access the generated boundary, use boundary.slice()
.
For the HTTP Content-Type header value for a boundary, use boundary.contentType()
. This returns a slice in the format
multipart/form-data; boundary=<boundary>
Creating a write stream just needs an underlying writer to write to:
const write_stream = wardrobe.writeStream(boundary, http_request.writer());
Using decl literals, you can write this in one line:
const write_stream = wardrobe.writeStream(.entropy("MyApplicationBoundary", prng.random()), http_request.writer());
Given a write stream, you have the following functions to write form data sections:
pub fn writer(self: *WriteStream) Writer;
pub fn beginTextEntry(self: *WriteStream, name: []const u8) !void;
pub fn beginFileEntry(self: *WriteStream, name: []const u8, content_type: []const u8, file_name: []const u8) !void;
pub fn endEntry(self: *WriteStream) !void;
pub fn endEntries(self: *WriteStream) !void;
There are runtime assertions to make sure you call functions in the right order. You can follow this pseudocode to know which functions to call:
for each entry:
write_stream.beginTextEntry() or write_stream.beginFileEntry()
write entry data with write_stream.writer()
write_stream.endEntry()
write_stream.endEntries();
Given a 'Content-Type' header, you can use Boundary.parseContentType
to get a boundary object:
const boundary = try wardrobe.Boundary.parseContentType("multipart/form-data; boundary=------Boundary");
The function returns error.Invalid
if the header is not valid for multipart/form-data
, or if the boundary
is too long.
Given a reader, you can iterate through the form data entries of a body. Note that there's no guarantee that the reader only reads what is necessary, it may overflow.
The Scanner API takes an allocator, but the allocations are only temporary.
var scanner = try scanner(allocator, boundary, reader);
defer scanner.deinit();
while (try scanner.nextEntry()) |entry| {
const data = scanner.reader().readAllAlloc(allocator, std.math.maxInt(usize));
defer std.testing.allocator.free(data);
}
scanner.reader()
returns a reader that gives EOF upon the end of the current active entry's data.
The returned entry has the following signature:
pub const Scanner.Entry = struct {
name: []const u8,
file_name: ?[]const u8,
content_type: ?[]const u8,
};
Sometimes, it may be useful to parse an entire response body or slice at once. Wardrobe provides utility functions
in wardrobe.parse
:
const entries = try wardrobe.parse.fromSlice(allocator, boundary, slice);
// or try wardrobe.parse.fromReader(allocator, boundary, reader);
// or try wardrobe.parse.fromScanner(allocator, boundary, scanner);
defer wardrobe.parse.deinitEntries(entries);
The entries returned is the same entry struct as in Scanner.Entry, but also has a data: []const u8
field
for accessing the whole parsed data.
Since you own all of the data and entries returned, you can use wardrobe.parse.deinitEntries
(or parse.Entry.deinit
for individual entries) to clean-up.
All Wardrobe code is under the MIT license.