Skip to content

Commit 6641604

Browse files
committed
fix(form): better parsing and API
1 parent 1aa668d commit 6641604

File tree

5 files changed

+136
-54
lines changed

5 files changed

+136
-54
lines changed

build.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ pub fn build(b: *std.Build) void {
4141
}
4242

4343
const tests = b.addTest(.{
44-
.name = "unit-test",
45-
.root_source_file = b.path("./src/unit_test.zig"),
44+
.name = "tests",
45+
.root_source_file = b.path("./src/tests.zig"),
4646
});
4747
tests.root_module.addImport("tardy", tardy);
4848
tests.root_module.linkLibrary(bearssl);

examples/form/main.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ const UserInfo = struct {
5151

5252
fn generate_handler(ctx: *const Context, _: void) !Respond {
5353
const info = switch (ctx.request.method.?) {
54-
.GET => try Query(UserInfo).parse(ctx),
55-
.POST => try Form(UserInfo).parse(ctx),
54+
.GET => try Query(UserInfo).parse(ctx.allocator, ctx),
55+
.POST => try Form(UserInfo).parse(ctx.allocator, ctx),
5656
else => return error.UnexpectedMethod,
5757
};
5858

src/http/form.zig

Lines changed: 125 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -28,37 +28,33 @@ pub fn decode_alloc(allocator: std.mem.Allocator, input: []const u8) ![]const u8
2828
return list.toOwnedSlice(allocator);
2929
}
3030

31-
fn parse_from(comptime name: []const u8, comptime T: type, value: []const u8) !T {
32-
switch (@typeInfo(T)) {
33-
.Int => |info| {
34-
return switch (info.signedness) {
35-
.unsigned => try std.fmt.parseUnsigned(T, value, 10),
36-
.signed => try std.fmt.parseInt(T, value, 10),
37-
};
38-
},
39-
.Float => |_| {
40-
return try std.fmt.parseFloat(T, value);
41-
},
42-
.Optional => |info| {
43-
return @as(T, try parse_from(name, info.child, value));
31+
fn parse_from(allocator: std.mem.Allocator, comptime T: type, comptime name: []const u8, value: []const u8) !T {
32+
return switch (@typeInfo(T)) {
33+
.Int => |info| switch (info.signedness) {
34+
.unsigned => try std.fmt.parseUnsigned(T, value, 10),
35+
.signed => try std.fmt.parseInt(T, value, 10),
4436
},
37+
.Float => try std.fmt.parseFloat(T, value),
38+
.Optional => |info| @as(T, try parse_from(allocator, info.child, name, value)),
39+
.Enum => std.meta.stringToEnum(T, value) orelse return error.InvalidEnumValue,
40+
.Bool => std.mem.eql(u8, value, "true"),
4541
else => switch (T) {
46-
[]const u8 => return value,
47-
bool => return std.mem.eql(u8, value, "true"),
42+
[]const u8 => try allocator.dupe(u8, value),
43+
[:0]const u8 => try allocator.dupeZ(u8, value),
4844
else => std.debug.panic("Unsupported field type \"{s}\"", .{@typeName(T)}),
4945
},
50-
}
46+
};
5147
}
5248

53-
fn parse_struct(comptime T: type, map: *const AnyCaseStringMap) !T {
49+
fn parse_struct(allocator: std.mem.Allocator, comptime T: type, map: *const AnyCaseStringMap) !T {
5450
var ret: T = undefined;
5551
assert(@typeInfo(T) == .Struct);
5652
const struct_info = @typeInfo(T).Struct;
5753
inline for (struct_info.fields) |field| {
58-
const maybe_value_str: ?[]const u8 = map.get(field.name);
54+
const entry = map.getEntry(field.name);
5955

60-
if (maybe_value_str) |value| {
61-
@field(ret, field.name) = try parse_from(field.name, field.type, value);
56+
if (entry) |e| {
57+
@field(ret, field.name) = try parse_from(allocator, field.type, field.name, e.value_ptr.*);
6258
} else if (field.default_value) |default| {
6359
@field(ret, field.name) = @as(*const field.type, @ptrCast(@alignCast(default))).*;
6460
} else if (@typeInfo(field.type) == .Optional) {
@@ -69,42 +65,128 @@ fn parse_struct(comptime T: type, map: *const AnyCaseStringMap) !T {
6965
return ret;
7066
}
7167

68+
fn construct_map_from_body(allocator: std.mem.Allocator, m: *AnyCaseStringMap, body: []const u8) !void {
69+
var pairs = std.mem.splitScalar(u8, body, '&');
70+
71+
while (pairs.next()) |pair| {
72+
const field_idx = std.mem.indexOfScalar(u8, pair, '=') orelse return error.MissingValue;
73+
if (pair.len < field_idx + 2) return error.MissingValue;
74+
75+
const key = pair[0..field_idx];
76+
const value = pair[(field_idx + 1)..];
77+
78+
if (std.mem.indexOfScalar(u8, value, '=') != null) return error.MalformedPair;
79+
80+
const decoded_key = try decode_alloc(allocator, key);
81+
errdefer allocator.free(decoded_key);
82+
83+
const decoded_value = try decode_alloc(allocator, value);
84+
errdefer allocator.free(decoded_value);
85+
86+
// Allow for duplicates (like with the URL params),
87+
// The last one just takes precedent.
88+
const entry = try m.getOrPut(decoded_key);
89+
if (entry.found_existing) {
90+
allocator.free(decoded_key);
91+
allocator.free(entry.value_ptr.*);
92+
}
93+
entry.value_ptr.* = decoded_value;
94+
}
95+
}
96+
7297
/// Parses Form data from a request body in `x-www-form-urlencoded` format.
7398
pub fn Form(comptime T: type) type {
7499
return struct {
75-
pub fn parse(ctx: *const Context) !T {
100+
pub fn parse(allocator: std.mem.Allocator, ctx: *const Context) !T {
76101
var m = AnyCaseStringMap.init(ctx.allocator);
77-
defer m.deinit();
78-
79-
const map: *const AnyCaseStringMap = map: {
80-
if (ctx.request.body) |body| {
81-
var pairs = std.mem.splitScalar(u8, body, '&');
82-
while (pairs.next()) |pair| {
83-
var kv = std.mem.splitScalar(u8, pair, '=');
102+
defer {
103+
var it = m.iterator();
104+
while (it.next()) |entry| {
105+
allocator.free(entry.key_ptr.*);
106+
allocator.free(entry.value_ptr.*);
107+
}
108+
m.deinit();
109+
}
84110

85-
const key = kv.next() orelse return error.MalformedForm;
86-
const decoded_key = try decode_alloc(ctx.allocator, key);
111+
if (ctx.request.body) |body|
112+
try construct_map_from_body(allocator, &m, body)
113+
else
114+
return error.BodyEmpty;
87115

88-
const value = kv.next() orelse return error.MalformedForm;
89-
const decoded_value = try decode_alloc(ctx.allocator, value);
90-
91-
assert(kv.next() == null);
92-
try m.putNoClobber(decoded_key, decoded_value);
93-
}
94-
} else return error.BodyEmpty;
95-
break :map &m;
96-
};
97-
98-
return parse_struct(T, map);
116+
return parse_struct(allocator, T, &m);
99117
}
100118
};
101119
}
102120

103121
/// Parses Form data from request URL query parameters.
104122
pub fn Query(comptime T: type) type {
105123
return struct {
106-
pub fn parse(ctx: *const Context) !T {
107-
return parse_struct(T, ctx.queries);
124+
pub fn parse(allocator: std.mem.Allocator, ctx: *const Context) !T {
125+
return parse_struct(allocator, T, ctx.queries);
108126
}
109127
};
110128
}
129+
130+
const testing = std.testing;
131+
132+
test "FormData: Parsing from Body" {
133+
const UserRole = enum { admin, visitor };
134+
const User = struct { id: u32, name: []const u8, age: u8, role: UserRole };
135+
const body: []const u8 = "id=10&name=John&age=12&role=visitor";
136+
137+
var m = AnyCaseStringMap.init(testing.allocator);
138+
defer {
139+
var it = m.iterator();
140+
while (it.next()) |entry| {
141+
testing.allocator.free(entry.key_ptr.*);
142+
testing.allocator.free(entry.value_ptr.*);
143+
}
144+
m.deinit();
145+
}
146+
try construct_map_from_body(testing.allocator, &m, body);
147+
148+
const parsed = try parse_struct(testing.allocator, User, &m);
149+
defer testing.allocator.free(parsed.name);
150+
151+
try testing.expectEqual(10, parsed.id);
152+
try testing.expectEqualSlices(u8, "John", parsed.name);
153+
try testing.expectEqual(12, parsed.age);
154+
try testing.expectEqual(UserRole.visitor, parsed.role);
155+
}
156+
157+
test "FormData: Parsing Missing Fields" {
158+
const User = struct { id: u32, name: []const u8, age: u8 };
159+
const body: []const u8 = "id=10";
160+
161+
var m = AnyCaseStringMap.init(testing.allocator);
162+
defer {
163+
var it = m.iterator();
164+
while (it.next()) |entry| {
165+
testing.allocator.free(entry.key_ptr.*);
166+
testing.allocator.free(entry.value_ptr.*);
167+
}
168+
m.deinit();
169+
}
170+
171+
try construct_map_from_body(testing.allocator, &m, body);
172+
173+
const parsed = parse_struct(testing.allocator, User, &m);
174+
try testing.expectError(error.FieldEmpty, parsed);
175+
}
176+
177+
test "FormData: Parsing Missing Value" {
178+
const body: []const u8 = "abc=abc&id=";
179+
180+
var m = AnyCaseStringMap.init(testing.allocator);
181+
defer {
182+
var it = m.iterator();
183+
while (it.next()) |entry| {
184+
testing.allocator.free(entry.key_ptr.*);
185+
testing.allocator.free(entry.value_ptr.*);
186+
}
187+
m.deinit();
188+
}
189+
190+
const result = construct_map_from_body(testing.allocator, &m, body);
191+
try testing.expectError(error.MissingValue, result);
192+
}

src/http/router/routing_trie.zig

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -273,14 +273,13 @@ pub const RoutingTrie = struct {
273273
var query_iter = std.mem.tokenizeScalar(u8, path[pos + 1 ..], '&');
274274

275275
while (query_iter.next()) |chunk| {
276-
const field_idx = std.mem.indexOfScalar(u8, chunk, '=') orelse return error.QueryMissingValue;
277-
if (chunk.len < field_idx + 2) return error.QueryMissingValue;
276+
const field_idx = std.mem.indexOfScalar(u8, chunk, '=') orelse return error.MissingValue;
277+
if (chunk.len < field_idx + 2) return error.MissingValue;
278278

279279
const key = chunk[0..field_idx];
280280
const value = chunk[(field_idx + 1)..];
281281

282-
if (std.mem.indexOfScalar(u8, key, '=') != null) return error.MalformedQueryKey;
283-
if (std.mem.indexOfScalar(u8, value, '=') != null) return error.MalformedQueryValue;
282+
if (std.mem.indexOfScalar(u8, value, '=') != null) return error.MalformedPair;
284283

285284
const decoded_key = try decode_alloc(allocator, key);
286285
try duped.append(allocator, decoded_key);
@@ -523,14 +522,14 @@ test "Routing with Queries" {
523522
q.clearRetainingCapacity();
524523
// Purposefully bad format with incomplete key/value pair.
525524
const captured = s.get_bundle(testing.allocator, "/item/100/price/283.21?help", captures[0..], &q);
526-
try testing.expectError(error.QueryMissingValue, captured);
525+
try testing.expectError(error.MissingValue, captured);
527526
}
528527

529528
{
530529
q.clearRetainingCapacity();
531530
// Purposefully bad format with incomplete key/value pair.
532531
const captured = s.get_bundle(testing.allocator, "/item/100/price/283.21?help=", captures[0..], &q);
533-
try testing.expectError(error.QueryMissingValue, captured);
532+
try testing.expectError(error.MissingValue, captured);
534533
}
535534

536535
{
@@ -542,6 +541,6 @@ test "Routing with Queries" {
542541
captures[0..],
543542
&q,
544543
);
545-
try testing.expectError(error.MalformedQueryValue, captured);
544+
try testing.expectError(error.MalformedPair, captured);
546545
}
547546
}

src/unit_test.zig renamed to src/tests.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ test "zzz unit tests" {
2020
testing.refAllDecls(@import("./http/server.zig"));
2121
testing.refAllDecls(@import("./http/sse.zig"));
2222
testing.refAllDecls(@import("./http/status.zig"));
23+
testing.refAllDecls(@import("./http/form.zig"));
2324

2425
// Router
2526
testing.refAllDecls(@import("./http/router.zig"));

0 commit comments

Comments
 (0)