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 }