lib/http/src/properties/message.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const hypothesis = @import("hypothesis");
3 const http = @import("http");
4
5 const Allocator = std.mem.Allocator;
6 const Request = http.Request;
7
8 const methods = [_][]const u8{
9 "GET",
10 "POST",
11 "PUT",
12 "DELETE",
13 "HEAD",
14 "OPTIONS",
15 "PATCH",
16 "CONNECT",
17 "TRACE",
18 };
19
20 const path_segments = [_][]const u8{
21 "/",
22 "/index.html",
23 "/a/b/c",
24 "/search?q=zig&page=2",
25 "/%20encoded",
26 "/UPPER/lower",
27 };
28
29 const header_values = [_][]const u8{
30 "0",
31 "text/plain",
32 "keep-alive",
33 " padded ",
34 "a=b; c=d",
35 "Sun, 06 Nov 1994 08:49:37 GMT",
36 };
37
38 pub fn settings(seed: u64) hypothesis.Settings {
39 return hypothesis.Settings.quick()
40 .withSeed(seed)
41 .withDatabase("zig-out/hypothesis-failures/http");
42 }
43
44 fn drawUsize(
45 data: *hypothesis.ConjectureData,
46 min: usize,
47 max: usize,
48 shrink_towards: usize,
49 ) !usize {
50 return @intCast(try data.drawInteger(
51 @intCast(min),
52 @intCast(max),
53 @intCast(shrink_towards),
54 ));
55 }
56
57 const GeneratedRequest = struct {
58 serialized: []const u8,
59 method_index: usize,
60 path: []const u8,
61 http_1_1: bool,
62 header_names: [][]const u8,
63 header_values: [][]const u8,
64 body: ?[]const u8,
65 };
66
67 fn drawRequest(data: *hypothesis.ConjectureData, arena: Allocator) !GeneratedRequest {
68 const method_index = try drawUsize(data, 0, methods.len - 1, 0);
69 const path = path_segments[try drawUsize(data, 0, path_segments.len - 1, 0)];
70 const http_1_1 = try data.drawBoolean();
71
72 const header_count = try drawUsize(data, 0, 5, 1);
73 const names = try arena.alloc([]const u8, header_count);
74 const values = try arena.alloc([]const u8, header_count);
75 for (names, values, 0..) |*name, *value, index| {
76 name.* = try std.fmt.allocPrint(arena, "X-Gen-{d}", .{index});
77 value.* = header_values[try drawUsize(data, 0, header_values.len - 1, 0)];
78 }
79
80 var body: ?[]const u8 = null;
81 if (try data.drawBoolean()) {
82 const len = try drawUsize(data, 0, 32, 0);
83 const raw = try data.drawBytes(len, len);
84 body = try arena.dupe(u8, raw);
85 }
86
87 var out: std.ArrayList(u8) = .empty;
88 const writer_version: []const u8 = if (http_1_1) "HTTP/1.1" else "HTTP/1.0";
89 try out.appendSlice(arena, methods[method_index]);
90 try out.append(arena, ' ');
91 try out.appendSlice(arena, path);
92 try out.append(arena, ' ');
93 try out.appendSlice(arena, writer_version);
94 try out.appendSlice(arena, "\r\n");
95 for (names, values) |name, value| {
96 try out.appendSlice(arena, name);
97 try out.appendSlice(arena, ": ");
98 try out.appendSlice(arena, value);
99 try out.appendSlice(arena, "\r\n");
100 }
101 if (body) |bytes| {
102 const line = try std.fmt.allocPrint(arena, "Content-Length: {d}\r\n", .{bytes.len});
103 try out.appendSlice(arena, line);
104 }
105 try out.appendSlice(arena, "\r\n");
106 if (body) |bytes| {
107 try out.appendSlice(arena, bytes);
108 }
109
110 return .{
111 .serialized = try out.toOwnedSlice(arena),
112 .method_index = method_index,
113 .path = path,
114 .http_1_1 = http_1_1,
115 .header_names = names,
116 .header_values = values,
117 .body = body,
118 };
119 }
120
121 fn expectMatches(request: *Request, generated: GeneratedRequest) !void {
122 try std.testing.expectEqualStrings(methods[generated.method_index], @tagName(request.method));
123 try std.testing.expectEqualStrings(generated.path, request.path);
124 try std.testing.expectEqual(generated.http_1_1, request.version == .http_1_1);
125 for (generated.header_names, generated.header_values) |name, value| {
126 const stored = request.headers.get(name) orelse return error.MissingHeader;
127 try std.testing.expectEqualStrings(std.mem.trim(u8, value, " \t"), stored);
128 }
129 if (generated.body) |bytes| {
130 try std.testing.expectEqualSlices(u8, bytes, request.body orelse return error.MissingBody);
131 } else {
132 try std.testing.expectEqual(@as(?[]const u8, null), request.body);
133 }
134 }
135
136 fn requestStorage(allocator: Allocator, count: usize) !http.RequestStorage {
137 var storage = try http.RequestStorage.init(allocator, .{
138 .request_count = count,
139 .header_count_per_request = http.default_request_header_count,
140 .header_line_bytes = http.default_request_header_line_bytes,
141 .body_bytes_per_request = http.default_request_body_bytes,
142 });
143 storage.activate();
144 return storage;
145 }
146
147 pub const RoundTripProperty = struct {
148 pub fn property(data: *hypothesis.ConjectureData, allocator: Allocator) !void {
149 var arena_state = std.heap.ArenaAllocator.init(allocator);
150 defer arena_state.deinit();
151 const arena = arena_state.allocator();
152
153 const first = try drawRequest(data, arena);
154 const second = try drawRequest(data, arena);
155
156 const stream = try std.mem.concat(arena, u8, &.{ first.serialized, second.serialized });
157
158 var storage = try requestStorage(allocator, 2);
159 defer storage.deinit(allocator);
160
161 var head = try Request.parse(try storage.request(0), stream);
162 try std.testing.expectEqual(first.serialized.len, head.consumed);
163 try expectMatches(&head.request, first);
164
165 var tail = try Request.parse(try storage.request(1), stream[head.consumed..]);
166 try std.testing.expectEqual(second.serialized.len, tail.consumed);
167 try expectMatches(&tail.request, second);
168
169 var replay = try Request.parse(try storage.request(0), stream[0..head.consumed]);
170 try std.testing.expectEqual(head.consumed, replay.consumed);
171 try expectMatches(&replay.request, first);
172 }
173 };
174
175 pub const RobustnessProperty = struct {
176 pub fn property(data: *hypothesis.ConjectureData, allocator: Allocator) !void {
177 var arena_state = std.heap.ArenaAllocator.init(allocator);
178 defer arena_state.deinit();
179 const arena = arena_state.allocator();
180
181 const style = try data.drawInteger(0, 1, 0);
182 const input: []const u8 = if (style == 0) blk: {
183 const len = try drawUsize(data, 0, 96, 8);
184 const raw = try data.drawBytes(len, len);
185 break :blk try arena.dupe(u8, raw);
186 } else blk: {
187 const generated = try drawRequest(data, arena);
188 const mutated = try arena.dupe(u8, generated.serialized);
189 const flips = try drawUsize(data, 1, 3, 1);
190 for (0..flips) |_| {
191 const at = try drawUsize(data, 0, mutated.len - 1, 0);
192 mutated[at] ^= @intCast(try data.drawInteger(1, 255, 1));
193 }
194 const keep = try drawUsize(data, 0, mutated.len, mutated.len);
195 break :blk mutated[0..keep];
196 };
197
198 var storage = try requestStorage(allocator, 1);
199 defer storage.deinit(allocator);
200 const parsed = Request.parse(try storage.request(0), input) catch return;
201 try std.testing.expect(parsed.consumed <= input.len);
202 }
203 };
204
205 test "pbt: request parsing round-trips and pipelines generated requests" {
206 try hypothesis.checkNamed(RoundTripProperty, "http-message-roundtrip", settings(0x477));
207 }
208
209 test "pbt: request parsing survives corruption with typed errors" {
210 try hypothesis.checkNamed(RobustnessProperty, "http-message-robustness", settings(0x478));
211 }