@@ -28,37 +28,33 @@ pub fn decode_alloc(allocator: std.mem.Allocator, input: []const u8) ![]const u8
28
28
return list .toOwnedSlice (allocator );
29
29
}
30
30
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 ),
44
36
},
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" ),
45
41
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 ),
48
44
else = > std .debug .panic ("Unsupported field type \" {s}\" " , .{@typeName (T )}),
49
45
},
50
- }
46
+ };
51
47
}
52
48
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 {
54
50
var ret : T = undefined ;
55
51
assert (@typeInfo (T ) == .Struct );
56
52
const struct_info = @typeInfo (T ).Struct ;
57
53
inline for (struct_info .fields ) | field | {
58
- const maybe_value_str : ? [] const u8 = map .get (field .name );
54
+ const entry = map .getEntry (field .name );
59
55
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 .* );
62
58
} else if (field .default_value ) | default | {
63
59
@field (ret , field .name ) = @as (* const field .type , @ptrCast (@alignCast (default ))).* ;
64
60
} else if (@typeInfo (field .type ) == .Optional ) {
@@ -69,42 +65,128 @@ fn parse_struct(comptime T: type, map: *const AnyCaseStringMap) !T {
69
65
return ret ;
70
66
}
71
67
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
+
72
97
/// Parses Form data from a request body in `x-www-form-urlencoded` format.
73
98
pub fn Form (comptime T : type ) type {
74
99
return struct {
75
- pub fn parse (ctx : * const Context ) ! T {
100
+ pub fn parse (allocator : std.mem.Allocator , ctx : * const Context ) ! T {
76
101
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
+ }
84
110
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 ;
87
115
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 );
99
117
}
100
118
};
101
119
}
102
120
103
121
/// Parses Form data from request URL query parameters.
104
122
pub fn Query (comptime T : type ) type {
105
123
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 );
108
126
}
109
127
};
110
128
}
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
+ }
0 commit comments