1236 lines
36 KiB
Zig
1236 lines
36 KiB
Zig
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
|
|
const t = @import("t.zig");
|
|
const http = @import("httpz.zig");
|
|
|
|
const Url = @import("url.zig").Url;
|
|
const Params = @import("params.zig").Params;
|
|
const KeyValue = @import("key_value.zig").KeyValue;
|
|
|
|
const Reader = std.io.Reader;
|
|
const Allocator = std.mem.Allocator;
|
|
const Stream = if (builtin.is_test) *t.Stream else std.net.Stream;
|
|
|
|
// this approach to matching method name comes from zhp
|
|
const GET_ = @bitCast(u32, [4]u8{'G', 'E', 'T', ' '});
|
|
const PUT_ = @bitCast(u32, [4]u8{'P', 'U', 'T', ' '});
|
|
const POST = @bitCast(u32, [4]u8{'P', 'O', 'S', 'T'});
|
|
const HEAD = @bitCast(u32, [4]u8{'H', 'E', 'A', 'D'});
|
|
const PATC = @bitCast(u32, [4]u8{'P', 'A', 'T', 'C'});
|
|
const DELE = @bitCast(u32, [4]u8{'D', 'E', 'L', 'E'});
|
|
const OPTI = @bitCast(u32, [4]u8{'O', 'P', 'T', 'I'});
|
|
const HTTP = @bitCast(u32, [4]u8{'H', 'T', 'T', 'P'});
|
|
const V1P0 = @bitCast(u32, [4]u8{'/', '1', '.', '0'});
|
|
const V1P1 = @bitCast(u32, [4]u8{'/', '1', '.', '1'});
|
|
|
|
pub const Config = struct {
|
|
max_body_size: usize = 1_048_576,
|
|
buffer_size: usize = 65_8536,
|
|
max_header_count: usize = 32,
|
|
max_param_count: usize = 10,
|
|
max_query_count: usize = 32,
|
|
};
|
|
|
|
pub const Request = struct {
|
|
// The URL of the request
|
|
url: Url,
|
|
|
|
// the underlying socket to read from
|
|
stream: Stream,
|
|
|
|
// Path params (extracted from the URL based on the route).
|
|
// Using req.param(NAME) is preferred.
|
|
params: Params,
|
|
|
|
// The headers of the request. Using req.header(NAME) is preferred.
|
|
headers: KeyValue,
|
|
|
|
// The request method.
|
|
method: http.Method,
|
|
|
|
// The request protocol.
|
|
protocol: http.Protocol,
|
|
|
|
// The maximum body that we'll allow, take from the config object when the
|
|
// request is first created.
|
|
max_body_size: usize,
|
|
|
|
// Whether or not the body has been read.
|
|
// Used for two reasons. First, we only lazily read the body.
|
|
// Second, for keepalive, if the body wasn't read as part of the normal
|
|
// request handling, we need to discard it from the stream.
|
|
bd_read: bool,
|
|
// The body of the request, if any.
|
|
bd: ?[]const u8,
|
|
|
|
// cannot use an optional on qs, because it's pre-allocated so always exists
|
|
qs_read: bool,
|
|
// The query string lookup.
|
|
qs: KeyValue,
|
|
|
|
// Where in static we currently are. This is needed so that we can tell if
|
|
// the body can be read directly in static or not (if there's enough space).
|
|
pos: usize,
|
|
|
|
// When parsing our header, we're reading as much data as possible from the
|
|
// the socket. This means we might read some of the body into our static buffer
|
|
// as part of the "header" parsing. We need to know how much of static is the
|
|
// already-read body so that when it comes time to actually read the body, we
|
|
// know how much we've already done.
|
|
header_overread: usize,
|
|
|
|
// A buffer that exists for the entire lifetime of the request. The sized
|
|
// is defined by the request.buffer_size configuration. The request header MUST
|
|
// fit in this size (requests with headers larger than this will be rejected).
|
|
// If possible, this space will also be used for the body.
|
|
static: []u8,
|
|
|
|
// An arena that will be reset at the end of each request. Can be used
|
|
// internally by this framework. The application is also free to make use of
|
|
// this arena. This is the same arena as response.arena.
|
|
arena: Allocator,
|
|
|
|
const Self = @This();
|
|
|
|
// Should not be called directly, but initialized through a pool, see server.zig reqResInit
|
|
pub fn init(self: *Self, allocator: Allocator, arena: Allocator, config: Config) !void {
|
|
self.pos = 0;
|
|
self.arena = arena;
|
|
self.stream = undefined;
|
|
self.max_body_size = config.max_body_size;
|
|
self.qs = try KeyValue.init(allocator, config.max_query_count);
|
|
|
|
self.static = try allocator.alloc(u8, config.buffer_size);
|
|
self.headers = try KeyValue.init(allocator, config.max_header_count);
|
|
self.params = try Params.init(allocator, config.max_param_count);
|
|
self.reset();
|
|
}
|
|
|
|
// Each parsing step (method, target, protocol, headers, body)
|
|
// return (a) how much data they've read from the socket and
|
|
// (b) how much data they've consumed. This informs the next step
|
|
// about what's available and where to start.
|
|
const ParseResult = struct {
|
|
// how much the step used of the buffer
|
|
used: usize,
|
|
|
|
// total data read from the socket (by a particular step)
|
|
read: usize,
|
|
};
|
|
|
|
pub fn deinit(self: *Self, allocator: Allocator) void {
|
|
self.qs.deinit(allocator);
|
|
self.params.deinit(allocator);
|
|
self.headers.deinit(allocator);
|
|
allocator.free(self.static);
|
|
}
|
|
|
|
pub fn reset(self: *Self) void {
|
|
self.bd_read = false;
|
|
self.bd = null;
|
|
|
|
self.qs_read = false;
|
|
self.qs.reset();
|
|
|
|
self.params.reset();
|
|
self.headers.reset();
|
|
}
|
|
|
|
pub fn header(self: *Self, name: []const u8) ?[]const u8 {
|
|
return self.headers.get(name);
|
|
}
|
|
|
|
pub fn param(self: *Self, name: []const u8) ?[]const u8 {
|
|
return self.params.get(name);
|
|
}
|
|
|
|
pub fn query(self: *Self) !KeyValue {
|
|
if (self.qs_read) {
|
|
return self.qs;
|
|
}
|
|
return self.parseQuery();
|
|
}
|
|
|
|
pub fn parse(self: *Self) !void {
|
|
// Header must always fits inside our static buffer
|
|
const buf = self.static;
|
|
const stream = self.stream;
|
|
|
|
var res = try self.parseMethod(stream, buf);
|
|
var pos = res.used;
|
|
var buf_len = res.read;
|
|
|
|
res = try self.parseURL(stream, buf[pos..], buf_len - pos);
|
|
pos += res.used;
|
|
buf_len += res.read;
|
|
|
|
res = try self.parseProtocol(stream, buf[pos..], buf_len - pos);
|
|
pos += res.used;
|
|
buf_len += res.read;
|
|
|
|
while (true) {
|
|
res = try self.parseOneHeader(stream, buf[pos..], buf_len - pos);
|
|
const used = res.used;
|
|
pos += used;
|
|
buf_len += res.read;
|
|
if (used == 2) { // it only consumed the trailing \r\n
|
|
break;
|
|
}
|
|
}
|
|
|
|
self.pos = pos;
|
|
self.header_overread = buf_len - pos;
|
|
}
|
|
|
|
pub fn body(self: *Self) !?[]const u8 {
|
|
if (self.bd_read) {
|
|
return self.bd;
|
|
}
|
|
|
|
self.bd_read = true;
|
|
const stream = self.stream;
|
|
|
|
if (self.header("content-length")) |cl| {
|
|
var length = atoi(cl) orelse return error.InvalidContentLength;
|
|
if (length == 0) {
|
|
self.bd = null;
|
|
return null;
|
|
}
|
|
|
|
if (length > self.max_body_size) {
|
|
return error.BodyTooBig;
|
|
}
|
|
|
|
const pos = self.pos;
|
|
|
|
// some (or all) of the body might have already been read into static
|
|
// when we were loading data as part of reading the header.
|
|
var read = self.header_overread;
|
|
|
|
if (read == length) {
|
|
// we're already read the entire body into static
|
|
self.bd = self.static[pos..(pos+read)];
|
|
self.pos += read;
|
|
return self.bd;
|
|
}
|
|
|
|
var buffer = self.static[pos..];
|
|
if (buffer.len >= length) {
|
|
self.pos = pos + length;
|
|
} else {
|
|
buffer = try self.arena.alloc(u8, length);
|
|
@memcpy(buffer[0..read], self.static[pos..(pos+read)]);
|
|
}
|
|
|
|
while (read < length) {
|
|
const n = try stream.read(buffer[read..]);
|
|
if (n == 0) {
|
|
return Error.ConnectionClosed;
|
|
}
|
|
read += n;
|
|
}
|
|
buffer = buffer[0..length];
|
|
self.bd = buffer;
|
|
return buffer;
|
|
}
|
|
// TODO: support chunked encoding
|
|
return self.bd;
|
|
}
|
|
|
|
pub fn json(self: *Self, comptime T: type) !?T {
|
|
const b = try self.body() orelse return null;
|
|
return try std.json.parseFromSlice(T, self.arena, b, .{});
|
|
}
|
|
|
|
pub fn jsonValueTree(self: *Self) !?std.json.ValueTree {
|
|
const b = try self.body() orelse return null;
|
|
var parser = std.json.Parser.init(self.arena, .alloc_always);
|
|
defer parser.deinit();
|
|
return try parser.parse(b);
|
|
}
|
|
|
|
pub fn jsonObject(self: *Self) !?std.json.ObjectMap {
|
|
const tree = try self.jsonValueTree() orelse return null;
|
|
switch (tree.root) {
|
|
.object => |o| return o,
|
|
else => return null,
|
|
}
|
|
}
|
|
|
|
pub fn jsonValue(self: *Self) !?std.json.Value {
|
|
const tree = try self.jsonValueTree() orelse return null;
|
|
return tree.root;
|
|
}
|
|
|
|
fn parseMethod(self: *Self, stream: Stream, buf: []u8) !ParseResult {
|
|
var buf_len: usize = 0;
|
|
while (buf_len < 4) {
|
|
buf_len += try readForHeader(stream, buf[buf_len..]);
|
|
}
|
|
|
|
while (true) {
|
|
const used = switch (@bitCast(u32, buf[0..4].*)) {
|
|
GET_ => {
|
|
self.method = .GET;
|
|
return .{.read = buf_len, .used = 4};
|
|
},
|
|
PUT_ => {
|
|
self.method = .PUT;
|
|
return .{.read = buf_len, .used = 4};
|
|
},
|
|
POST => {
|
|
// only need 1 more byte, so at most, we need 1 more read
|
|
if (buf_len < 5) buf_len += try readForHeader(stream, buf[buf_len..]);
|
|
if (buf[4] != ' ') {
|
|
return error.UnknownMethod;
|
|
}
|
|
self.method = .POST;
|
|
return .{.read = buf_len, .used = 5};
|
|
},
|
|
HEAD => {
|
|
// only need 1 more byte, so at most, we need 1 more read
|
|
if (buf_len < 5) buf_len += try readForHeader(stream, buf[buf_len..]);
|
|
if (buf[4] != ' ') {
|
|
return error.UnknownMethod;
|
|
}
|
|
self.method = .HEAD;
|
|
return .{.read = buf_len, .used = 5};
|
|
},
|
|
PATC => {
|
|
while (buf_len < 6) buf_len += try readForHeader(stream, buf[buf_len..]);
|
|
if (buf[4] != 'H' or buf[5] != ' ') {
|
|
return error.UnknownMethod;
|
|
}
|
|
self.method = .PATCH;
|
|
return .{.read = buf_len, .used = 6};
|
|
},
|
|
DELE => {
|
|
while (buf_len < 7) buf_len += try readForHeader(stream, buf[buf_len..]);
|
|
if (buf[4] != 'T' or buf[5] != 'E' or buf[6] != ' ' ) {
|
|
return error.UnknownMethod;
|
|
}
|
|
self.method = .DELETE;
|
|
return .{.read = buf_len, .used = 7};
|
|
},
|
|
OPTI => {
|
|
while (buf_len < 8) buf_len += try readForHeader(stream, buf[buf_len..]);
|
|
if (buf[4] != 'O' or buf[5] != 'N' or buf[6] != 'S' or buf[7] != ' ' ) {
|
|
return error.UnknownMethod;
|
|
}
|
|
self.method = .OPTIONS;
|
|
return .{.read = buf_len, .used = 8};
|
|
},
|
|
else => return error.UnknownMethod,
|
|
};
|
|
|
|
return ParseResult{
|
|
.used = used,
|
|
.read = buf_len,
|
|
};
|
|
}
|
|
}
|
|
|
|
fn parseURL(self: *Self, stream: Stream, buf: []u8, len: usize) !ParseResult {
|
|
var buf_len = len;
|
|
if (buf_len == 0) {
|
|
buf_len = try readForHeader(stream, buf);
|
|
}
|
|
|
|
switch (buf[0]) {
|
|
'/' => {
|
|
while (true) {
|
|
if (std.mem.indexOfScalar(u8, buf[0..buf_len], ' ')) |end_index| {
|
|
self.url = Url.parse(buf[0..end_index]);
|
|
// +1 to consume the space
|
|
return .{.used = end_index + 1, .read = buf_len - len};
|
|
}
|
|
buf_len += try readForHeader(stream, buf[buf_len..]);
|
|
}
|
|
},
|
|
'*' => {
|
|
// must be a "* ", so we need at least 1 more byte
|
|
if (buf_len == 1) {
|
|
buf_len += try readForHeader(stream, buf[buf_len..]);
|
|
}
|
|
// Read never returns 0, so if we're here, buf.len >= 1
|
|
if (buf[1] != ' ') {
|
|
return error.InvalidRequestTarget;
|
|
}
|
|
self.url = Url.star();
|
|
return .{.used = 2, .read = buf_len - len};
|
|
},
|
|
// TODO: Support absolute-form target (e.g. http://....)
|
|
else => return error.InvalidRequestTarget,
|
|
}
|
|
}
|
|
|
|
fn parseProtocol(self: *Self, stream: Stream, buf: []u8, len: usize) !ParseResult {
|
|
var buf_len = len;
|
|
while (buf_len < 10) {
|
|
buf_len += try readForHeader(stream, buf[buf_len..]);
|
|
}
|
|
if (@bitCast(u32, buf[0..4].*) != HTTP) {
|
|
return error.UnknownProtocol;
|
|
}
|
|
switch (@bitCast(u32, buf[4..8].*)) {
|
|
V1P1 => self.protocol = http.Protocol.HTTP11,
|
|
V1P0 => self.protocol = http.Protocol.HTTP10,
|
|
else => return error.UnsupportedProtocol,
|
|
}
|
|
|
|
if (buf[8] != '\r' or buf [9] != '\n') {
|
|
return error.UnknownProtocol;
|
|
}
|
|
|
|
return .{.read = buf_len - len, .used = 10};
|
|
}
|
|
|
|
fn parseOneHeader(self: *Self, stream: Stream, buf: []u8, len: usize) !ParseResult {
|
|
var buf_len = len;
|
|
while (true) {
|
|
if (findCarriageReturnIndex(buf[0..buf_len])) |header_end| {
|
|
|
|
const next = header_end + 1;
|
|
if (next == buf_len) buf_len += try readForHeader(stream, buf[buf_len..]);
|
|
|
|
if (buf[next] != '\n') {
|
|
return error.InvalidHeaderLine;
|
|
}
|
|
|
|
if (header_end == 0) {
|
|
return .{.read = buf_len - len, .used = 2};
|
|
}
|
|
|
|
for (buf[0..header_end], 0..) |b, i| {
|
|
// find the colon and lowercase the header while we're iterating
|
|
if ('A' <= b and b <= 'Z') {
|
|
buf[i] = b + 32;
|
|
continue;
|
|
}
|
|
if (b != ':') {
|
|
continue;
|
|
}
|
|
|
|
const name = buf[0..i];
|
|
const value = trimLeadingSpace(buf[i+1..header_end]);
|
|
self.headers.add(name, value);
|
|
return .{.read = buf_len - len, .used = next + 1};
|
|
}
|
|
return error.InvalidHeaderLine;
|
|
}
|
|
buf_len += try readForHeader(stream, buf[buf_len..]);
|
|
}
|
|
}
|
|
|
|
// OK, this is a bit complicated.
|
|
// We might need to allocate memory to parse the querystring. Specifically, if
|
|
// there's a url-escaped component (a key or value), we need memory to store
|
|
// the un-escaped version. Ideally, we'd like to use our static buffer for this
|
|
// but, and this is where it gets complicated, we might:
|
|
// 1 - Not have enough space
|
|
// 2 - Might have over-read the header and have part (or all) of the body in there
|
|
// The 1st case is easy: if we have space in the static buffer, we use it. If
|
|
// we don't, we allocate space in our arena. The space of an un-escaped component
|
|
// is always < than space of the original, so we can determine this easily.
|
|
// The 2nd case, where the static buffer is being used by the body as well, is
|
|
// where things get tricky. We could try to be smart and figure out: is there
|
|
// a body? Did we read it all? Did we partially read it but have the length?
|
|
// Instead, for now, we just load the body. The net result is that whatever
|
|
// free space we have left in the static buffer, we can use for this.
|
|
// It's a simple solution and it causes virtually no overhead in the two most
|
|
// common cases: (1) there is no body (2) there is a body and the user want it.
|
|
// The only case where this makes it inefficient is where:
|
|
// (there is a body AND the user doesn't want it) AND
|
|
// (no keepalive OR (keepalive AND body > buffer)).
|
|
// If you're wondering why keepalive matters? It's because, with keepalive
|
|
// (which we generally expect to be set), the body needs to be read anyways.
|
|
fn parseQuery(self: *Self) !KeyValue {
|
|
const raw = self.url.query;
|
|
if (raw.len == 0) {
|
|
self.qs_read = true;
|
|
return self.qs;
|
|
}
|
|
|
|
_ = try self.body();
|
|
|
|
var qs = &self.qs;
|
|
var pos = self.pos;
|
|
var allocator = self.arena;
|
|
var buffer = self.static[pos..];
|
|
|
|
var it = std.mem.split(u8, raw, "&");
|
|
while (it.next()) |pair| {
|
|
if (std.mem.indexOfScalar(u8, pair, '=')) |sep| {
|
|
const key_res = try Url.unescape(allocator, buffer, pair[0..sep]);
|
|
if (key_res.buffered) {
|
|
const n = key_res.value.len;
|
|
pos += n;
|
|
buffer = buffer[n..];
|
|
}
|
|
|
|
const value_res = try Url.unescape(allocator, buffer, pair[sep+1..]);
|
|
if (value_res.buffered) {
|
|
const n = value_res.value.len;
|
|
pos += n;
|
|
buffer = buffer[n..];
|
|
}
|
|
qs.add(key_res.value, value_res.value);
|
|
} else {
|
|
const key_res = try Url.unescape(allocator, buffer, pair);
|
|
if (key_res.buffered) {
|
|
const n = key_res.value.len;
|
|
pos += n;
|
|
buffer = buffer[n..];
|
|
}
|
|
qs.add(key_res.value, "");
|
|
}
|
|
}
|
|
|
|
self.pos = pos;
|
|
self.qs_read = true;
|
|
return self.qs;
|
|
}
|
|
|
|
// Drains the body from the socket (if it hasn't already been read). This is
|
|
// only necessary during keepalive requests. We don't care about the contents
|
|
// of the body, we just want to move the socket to end of this request (which
|
|
// would be the start of the next one).
|
|
// We assume the request will be reset after this is called, so its ok for us
|
|
// to c the static buffer (which various header elements point to)
|
|
pub fn drain(self: *Self) !void {
|
|
if (self.bd_read == true) {
|
|
// body has already been read
|
|
return;
|
|
}
|
|
|
|
const stream = self.stream;
|
|
if (self.header("content-length")) |value| {
|
|
var buffer = self.static;
|
|
var length = atoi(value) orelse return error.InvalidContentLength;
|
|
|
|
// some (or all) of the body might have already been read into static
|
|
// when we were loading data as part of reading the header.
|
|
length -= self.header_overread;
|
|
|
|
while (length > 0) {
|
|
var n = if (buffer.len > length) buffer[0..length] else buffer;
|
|
length -= try stream.read(n);
|
|
}
|
|
} else {
|
|
// TODO: support chunked encoding
|
|
}
|
|
|
|
// This should not be necessary, since we expect the request to be reset
|
|
// immediately after drain is called. However, it's a cheap thing
|
|
// to add which might prevent weird accidental interactions.
|
|
self.bd_read = true;
|
|
}
|
|
|
|
pub fn canKeepAlive(self: *Self) bool {
|
|
return switch (self.protocol) {
|
|
http.Protocol.HTTP11 => {
|
|
if (self.headers.get("connection")) |conn| {
|
|
return !std.mem.eql(u8, conn, "close");
|
|
}
|
|
return true;
|
|
},
|
|
http.Protocol.HTTP10 => return false, // TODO: support this in the cases where it can be
|
|
};
|
|
}
|
|
};
|
|
|
|
inline fn trimLeadingSpace(in: []const u8) []const u8 {
|
|
// very common case where we have a single space after our colon
|
|
if (in.len >= 2 and in[0] == ' ' and in[1] != ' ') return in[1..];
|
|
|
|
for (in, 0..) |b, i| {
|
|
if (b != ' ') return in[i..];
|
|
}
|
|
return "";
|
|
}
|
|
|
|
fn readForHeader(stream: Stream, buffer: []u8) !usize {
|
|
const n = try stream.read(buffer);
|
|
if (n == 0) {
|
|
if (buffer.len == 0) {
|
|
return error.HeaderTooBig;
|
|
}
|
|
return error.ConnectionClosed;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
fn atoi(str: []const u8) ?usize {
|
|
if (str.len == 0) {
|
|
return null;
|
|
}
|
|
|
|
var n: usize = 0;
|
|
for (str) |b| {
|
|
var d = b - '0';
|
|
if (d > 9) {
|
|
return null;
|
|
}
|
|
n = n * 10 + @intCast(usize, d);
|
|
}
|
|
return n;
|
|
}
|
|
|
|
const CR = '\r';
|
|
const VECTOR_8_LEN = if (std.simd.suggestVectorSize(u8) == null) 0 else 8;
|
|
const VECTOR_8_CR = @splat(VECTOR_8_LEN, @as(u8, CR));
|
|
const VECTOR_8_IOTA = std.simd.iota(u8, VECTOR_8_LEN);
|
|
const VECTOR_8_NULLS = @splat(VECTOR_8_LEN, @as(u8, 255));
|
|
|
|
const VECTOR_16_LEN = if (std.simd.suggestVectorSize(u8) == null) 0 else 16;
|
|
const VECTOR_16_CR = @splat(VECTOR_16_LEN, @as(u8, CR));
|
|
const VECTOR_16_IOTA = std.simd.iota(u8, VECTOR_16_LEN);
|
|
const VECTOR_16_NULLS = @splat(VECTOR_16_LEN, @as(u8, 255));
|
|
|
|
const VECTOR_32_LEN = if (std.simd.suggestVectorSize(u8) == null) 0 else 32;
|
|
const VECTOR_32_CR = @splat(VECTOR_32_LEN, @as(u8, CR));
|
|
const VECTOR_32_IOTA = std.simd.iota(u8, VECTOR_32_LEN);
|
|
const VECTOR_32_NULLS = @splat(VECTOR_32_LEN, @as(u8, 255));
|
|
|
|
const VECTOR_64_LEN = if (std.simd.suggestVectorSize(u8) == null) 0 else 64;
|
|
const VECTOR_64_CR = @splat(VECTOR_64_LEN, @as(u8, CR));
|
|
const VECTOR_64_IOTA = std.simd.iota(u8, VECTOR_64_LEN);
|
|
const VECTOR_64_NULLS = @splat(VECTOR_64_LEN, @as(u8, 255));
|
|
|
|
fn findCarriageReturnIndex(buf: []u8) ?usize {
|
|
if (VECTOR_32_LEN == 0) {
|
|
return std.mem.indexOfScalar(u8, buf, CR);
|
|
}
|
|
|
|
var pos: usize = 0;
|
|
var left = buf.len;
|
|
while (left > 0) {
|
|
if (left < VECTOR_8_LEN) {
|
|
if (std.mem.indexOfScalar(u8, buf[pos..], CR)) |n| {
|
|
return pos + n;
|
|
}
|
|
return null;
|
|
}
|
|
var index: u8 = undefined;
|
|
var vector_len: usize = undefined;
|
|
if (left < VECTOR_16_LEN) {
|
|
vector_len = VECTOR_8_LEN;
|
|
const vec: @Vector(VECTOR_8_LEN, u8) = buf[pos..][0..VECTOR_8_LEN].*;
|
|
const matches = vec == VECTOR_8_CR;
|
|
const indices = @select(u8, matches, VECTOR_8_IOTA, VECTOR_8_NULLS);
|
|
index = @reduce(.Min, indices);
|
|
} else if (left < VECTOR_32_LEN) {
|
|
vector_len = VECTOR_16_LEN;
|
|
const vec: @Vector(VECTOR_16_LEN, u8) = buf[pos..][0..VECTOR_16_LEN].*;
|
|
const matches = vec == VECTOR_16_CR;
|
|
const indices = @select(u8, matches, VECTOR_16_IOTA, VECTOR_16_NULLS);
|
|
index = @reduce(.Min, indices);
|
|
} else if (left < VECTOR_64_LEN) {
|
|
vector_len = VECTOR_32_LEN;
|
|
const vec: @Vector(VECTOR_32_LEN, u8) = buf[pos..][0..VECTOR_32_LEN].*;
|
|
const matches = vec == VECTOR_32_CR;
|
|
const indices = @select(u8, matches, VECTOR_32_IOTA, VECTOR_32_NULLS);
|
|
index = @reduce(.Min, indices);
|
|
} else {
|
|
vector_len = VECTOR_64_LEN;
|
|
const vec: @Vector(VECTOR_64_LEN, u8) = buf[pos..][0..VECTOR_64_LEN].*;
|
|
const matches = vec == VECTOR_64_CR;
|
|
const indices = @select(u8, matches, VECTOR_64_IOTA, VECTOR_64_NULLS);
|
|
index = @reduce(.Min, indices);
|
|
}
|
|
|
|
if (index != 255) {
|
|
return pos + index;
|
|
}
|
|
pos += vector_len;
|
|
left -= vector_len;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
test "atoi" {
|
|
var buf: [5]u8 = undefined;
|
|
for (0..99999) |i| {
|
|
const n = std.fmt.formatIntBuf(&buf, i, 10, .lower, .{});
|
|
try t.expectEqual(@intCast(usize, i), atoi(buf[0..n]).?);
|
|
}
|
|
|
|
try t.expectEqual(@as(?usize, null), atoi(""));
|
|
try t.expectEqual(@as(?usize, null), atoi("392a"));
|
|
try t.expectEqual(@as(?usize, null), atoi("b392"));
|
|
try t.expectEqual(@as(?usize, null), atoi("3c92"));
|
|
}
|
|
|
|
test "request: findCarriageReturnIndex" {
|
|
var input = ("z" ** 128).*;
|
|
for (1..input.len) |i| {
|
|
var buf = input[0..i];
|
|
try t.expectEqual(@as(?usize, null), findCarriageReturnIndex(buf));
|
|
|
|
for (0..i) |j| {
|
|
buf[j] = CR;
|
|
if (j > 0) {
|
|
buf[j-1] = 'z';
|
|
}
|
|
try t.expectEqual(j, findCarriageReturnIndex(buf).?);
|
|
}
|
|
buf[i-1] = 'z';
|
|
}
|
|
}
|
|
|
|
const Error = error {
|
|
BodyTooBig,
|
|
HeaderTooBig,
|
|
ConnectionClosed,
|
|
UnknownMethod,
|
|
InvalidRequestTarget,
|
|
UnknownProtocol,
|
|
UnsupportedProtocol,
|
|
InvalidHeaderLine,
|
|
InvalidContentLength,
|
|
};
|
|
|
|
test "request: header too big" {
|
|
try expectParseError(Error.HeaderTooBig, "GET / HTTP/1.1\r\n\r\n", .{.buffer_size = 17});
|
|
try expectParseError(Error.HeaderTooBig, "GET / HTTP/1.1\r\nH: v\r\n\r\n", .{.buffer_size = 23});
|
|
}
|
|
|
|
test "request: parse method" {
|
|
{
|
|
try expectParseError(Error.ConnectionClosed, "GET", .{});
|
|
try expectParseError(Error.UnknownMethod, "GETT ", .{});
|
|
try expectParseError(Error.UnknownMethod, " PUT ", .{});
|
|
}
|
|
|
|
{
|
|
const r = testParse("GET / HTTP/1.1\r\n\r\n", .{});
|
|
defer testCleanup(r);
|
|
try t.expectEqual(http.Method.GET, r.method);
|
|
}
|
|
|
|
{
|
|
const r = testParse("PUT / HTTP/1.1\r\n\r\n", .{});
|
|
defer testCleanup(r);
|
|
try t.expectEqual(http.Method.PUT, r.method);
|
|
}
|
|
|
|
{
|
|
const r = testParse("POST / HTTP/1.1\r\n\r\n", .{});
|
|
defer testCleanup(r);
|
|
try t.expectEqual(http.Method.POST, r.method);
|
|
}
|
|
|
|
{
|
|
const r = testParse("HEAD / HTTP/1.1\r\n\r\n", .{});
|
|
defer testCleanup(r);
|
|
try t.expectEqual(http.Method.HEAD, r.method);
|
|
}
|
|
|
|
{
|
|
const r = testParse("PATCH / HTTP/1.1\r\n\r\n", .{});
|
|
defer testCleanup(r);
|
|
try t.expectEqual(http.Method.PATCH, r.method);
|
|
}
|
|
|
|
{
|
|
const r = testParse("DELETE / HTTP/1.1\r\n\r\n", .{});
|
|
defer testCleanup(r);
|
|
try t.expectEqual(http.Method.DELETE, r.method);
|
|
}
|
|
|
|
{
|
|
const r = testParse("OPTIONS / HTTP/1.1\r\n\r\n", .{});
|
|
defer testCleanup(r);
|
|
try t.expectEqual(http.Method.OPTIONS, r.method);
|
|
}
|
|
}
|
|
|
|
test "request: parse request target" {
|
|
{
|
|
try expectParseError(Error.InvalidRequestTarget, "GET NOPE", .{});
|
|
try expectParseError(Error.InvalidRequestTarget, "GET nope ", .{});
|
|
try expectParseError(Error.InvalidRequestTarget, "GET http://www.pondzpondz.com/test ", .{}); // this should be valid
|
|
try expectParseError(Error.InvalidRequestTarget, "PUT hello ", .{});
|
|
try expectParseError(Error.InvalidRequestTarget, "POST /hello ", .{});
|
|
try expectParseError(Error.InvalidRequestTarget, "POST *hello ", .{});
|
|
}
|
|
|
|
{
|
|
const r = testParse("PUT / HTTP/1.1\r\n\r\n", .{});
|
|
defer testCleanup(r);
|
|
try t.expectString("/", r.url.raw);
|
|
}
|
|
|
|
{
|
|
const r = testParse("PUT /api/v2 HTTP/1.1\r\n\r\n", .{});
|
|
defer testCleanup(r);
|
|
try t.expectString("/api/v2", r.url.raw);
|
|
}
|
|
|
|
{
|
|
const r = testParse("DELETE /API/v2?hack=true&over=9000%20!! HTTP/1.1\r\n\r\n", .{});
|
|
defer testCleanup(r);
|
|
try t.expectString("/API/v2?hack=true&over=9000%20!!", r.url.raw);
|
|
}
|
|
|
|
{
|
|
const r = testParse("PUT * HTTP/1.1\r\n\r\n", .{});
|
|
defer testCleanup(r);
|
|
try t.expectString("*", r.url.raw);
|
|
}
|
|
}
|
|
|
|
test "request: parse protocol" {
|
|
{
|
|
try expectParseError(Error.ConnectionClosed, "GET / ", .{});
|
|
try expectParseError(Error.ConnectionClosed, "GET / ", .{});
|
|
try expectParseError(Error.ConnectionClosed, "GET / H\r\n", .{});
|
|
try expectParseError(Error.UnknownProtocol, "GET / http/1.1\r\n", .{});
|
|
try expectParseError(Error.UnsupportedProtocol, "GET / HTTP/2.0\r\n", .{});
|
|
}
|
|
|
|
{
|
|
const r = testParse("PUT / HTTP/1.0\r\n\r\n", .{});
|
|
defer testCleanup(r);
|
|
try t.expectEqual(http.Protocol.HTTP10, r.protocol);
|
|
}
|
|
|
|
{
|
|
const r = testParse("PUT / HTTP/1.1\r\n\r\n", .{});
|
|
defer testCleanup(r);
|
|
try t.expectEqual(http.Protocol.HTTP11, r.protocol);
|
|
}
|
|
}
|
|
|
|
test "request: parse headers" {
|
|
{
|
|
try expectParseError(Error.ConnectionClosed, "GET / HTTP/1.1\r\nH", .{});
|
|
try expectParseError(Error.InvalidHeaderLine, "GET / HTTP/1.1\r\nHost\r\n", .{});
|
|
try expectParseError(Error.ConnectionClosed, "GET / HTTP/1.1\r\nHost:another\r\n\r", .{});
|
|
try expectParseError(Error.ConnectionClosed, "GET / HTTP/1.1\r\nHost: pondzpondz.com\r\n", .{});
|
|
}
|
|
|
|
{
|
|
const r = testParse("PUT / HTTP/1.0\r\n\r\n", .{});
|
|
defer testCleanup(r);
|
|
try t.expectEqual(@as(usize, 0), r.headers.len);
|
|
}
|
|
|
|
{
|
|
const r = testParse("PUT / HTTP/1.0\r\nHost: pondzpondz.com\r\n\r\n", .{});
|
|
defer testCleanup(r);
|
|
|
|
try t.expectEqual(@as(usize, 1), r.headers.len);
|
|
try t.expectString("pondzpondz.com", r.headers.get("host").?);
|
|
}
|
|
|
|
{
|
|
const r = testParse("PUT / HTTP/1.0\r\nHost: pondzpondz.com\r\nMisc: Some-Value\r\nAuthorization:none\r\n\r\n", .{});
|
|
defer testCleanup(r);
|
|
|
|
try t.expectEqual(@as(usize, 3), r.headers.len);
|
|
try t.expectString("pondzpondz.com", r.header("host").?);
|
|
try t.expectString("Some-Value", r.header("misc").?);
|
|
try t.expectString("none", r.header("authorization").?);
|
|
}
|
|
}
|
|
|
|
test "request: canKeepAlive" {
|
|
{
|
|
// implicitly keepalive for 1.1
|
|
const r = testParse("GET / HTTP/1.1\r\n\r\n", .{});
|
|
defer testCleanup(r);
|
|
try t.expectEqual(true, r.canKeepAlive());
|
|
}
|
|
|
|
{
|
|
// explicitly keepalive for 1.1
|
|
const r = testParse("GET / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n", .{});
|
|
defer testCleanup(r);
|
|
try t.expectEqual(true, r.canKeepAlive());
|
|
}
|
|
|
|
{
|
|
// explicitly not keepalive for 1.1
|
|
const r = testParse("GET / HTTP/1.1\r\nConnection: close\r\n\r\n", .{});
|
|
defer testCleanup(r);
|
|
try t.expectEqual(false, r.canKeepAlive());
|
|
}
|
|
}
|
|
|
|
test "request: query" {
|
|
{
|
|
// none
|
|
const r = testParse("PUT / HTTP/1.1\r\n\r\n", .{});
|
|
defer testCleanup(r);
|
|
try t.expectEqual(@as(usize, 0), (try r.query()).len);
|
|
}
|
|
|
|
{
|
|
// none with path
|
|
const r = testParse("PUT /why/would/this/matter HTTP/1.1\r\n\r\n", .{});
|
|
defer testCleanup(r);
|
|
try t.expectEqual(@as(usize, 0), (try r.query()).len);
|
|
}
|
|
|
|
{
|
|
// value-less
|
|
const r = testParse("PUT /?a HTTP/1.1\r\n\r\n", .{});
|
|
defer testCleanup(r);
|
|
const query = try r.query();
|
|
try t.expectEqual(@as(usize, 1), query.len);
|
|
try t.expectString("", query.get("a").?);
|
|
try t.expectEqual(@as(?[]const u8, null), query.get("b"));
|
|
}
|
|
|
|
{
|
|
// single
|
|
const r = testParse("PUT /?a=1 HTTP/1.1\r\n\r\n", .{});
|
|
defer testCleanup(r);
|
|
const query = try r.query();
|
|
try t.expectEqual(@as(usize, 1), query.len);
|
|
try t.expectString("1", query.get("a").?);
|
|
try t.expectEqual(@as(?[]const u8, null), query.get("b"));
|
|
}
|
|
|
|
{
|
|
// multiple
|
|
const r = testParse("PUT /path?Teg=Tea&it%20%20IS=over%209000%24&ha%09ck HTTP/1.1\r\n\r\n", .{});
|
|
defer testCleanup(r);
|
|
const query = try r.query();
|
|
try t.expectEqual(@as(usize, 3), query.len);
|
|
try t.expectString("Tea", query.get("Teg").?);
|
|
try t.expectString("over 9000$", query.get("it IS").?);
|
|
try t.expectString("", query.get("ha\tck").?);
|
|
}
|
|
}
|
|
|
|
test "request: body content-length" {
|
|
{
|
|
// too big
|
|
const r = testParse("POST / HTTP/1.0\r\nContent-Length: 10\r\n\r\nOver 9000!", .{.max_body_size = 9});
|
|
defer testCleanup(r);
|
|
try t.expectError(Error.BodyTooBig, r.body());
|
|
}
|
|
|
|
{
|
|
// no body
|
|
const r = testParse("PUT / HTTP/1.0\r\nHost: pondzpondz.com\r\nContent-Length: 0\r\n\r\n", .{.max_body_size = 10});
|
|
defer testCleanup(r);
|
|
try t.expectEqual(@as(?[]const u8, null), try r.body());
|
|
try t.expectEqual(@as(?[]const u8, null), try r.body());
|
|
}
|
|
|
|
{
|
|
// fits into static buffer
|
|
const r = testParse("POST / HTTP/1.0\r\nContent-Length: 10\r\n\r\nOver 9000!", .{});
|
|
defer testCleanup(r);
|
|
try t.expectString("Over 9000!", (try r.body()).?);
|
|
try t.expectString("Over 9000!", (try r.body()).?);
|
|
}
|
|
|
|
{
|
|
// Requires dynamic buffer
|
|
const r = testParse("POST / HTTP/1.0\r\nContent-Length: 11\r\n\r\nOver 9001!!", .{.buffer_size = 40 });
|
|
defer testCleanup(r);
|
|
try t.expectString("Over 9001!!", (try r.body()).?);
|
|
try t.expectString("Over 9001!!", (try r.body()).?);
|
|
}
|
|
}
|
|
|
|
// the query and body both (can) occupy space in our static buffer, so we want
|
|
// to make sure they both work regardless of the order they're useds
|
|
test "request: query & body" {
|
|
{
|
|
// query then body
|
|
const r = testParse("POST /?search=keemun%20tea HTTP/1.0\r\nContent-Length: 10\r\n\r\nOver 9000!", .{});
|
|
defer testCleanup(r);
|
|
try t.expectString("keemun tea", (try r.query()).get("search").?);
|
|
try t.expectString("Over 9000!", (try r.body()).?);
|
|
|
|
// results should be cached internally, but let's double check
|
|
try t.expectString("keemun tea", (try r.query()).get("search").?);
|
|
try t.expectString("Over 9000!", (try r.body()).?);
|
|
}
|
|
|
|
{
|
|
// body then query
|
|
const r = testParse("POST /?search=keemun%20tea HTTP/1.0\r\nContent-Length: 10\r\n\r\nOver 9000!", .{});
|
|
defer testCleanup(r);
|
|
try t.expectString("Over 9000!", (try r.body()).?);
|
|
try t.expectString("keemun tea", (try r.query()).get("search").?);
|
|
|
|
// results should be cached internally, but let's double check
|
|
try t.expectString("Over 9000!", (try r.body()).?);
|
|
try t.expectString("keemun tea", (try r.query()).get("search").?);
|
|
}
|
|
}
|
|
|
|
test "body: json" {
|
|
const Tea = struct {
|
|
type: []const u8,
|
|
};
|
|
|
|
{
|
|
// too big
|
|
const r = testParse("POST / HTTP/1.0\r\nContent-Length: 17\r\n\r\n{\"type\":\"keemun\"}", .{.max_body_size = 16});
|
|
defer testCleanup(r);
|
|
try t.expectError(Error.BodyTooBig, r.json(Tea));
|
|
}
|
|
|
|
{
|
|
// no body
|
|
const r = testParse("PUT / HTTP/1.0\r\nHost: pondzpondz.com\r\nContent-Length: 0\r\n\r\n", .{.max_body_size = 10});
|
|
defer testCleanup(r);
|
|
try t.expectEqual(@as(?Tea, null), try r.json(Tea));
|
|
try t.expectEqual(@as(?Tea, null), try r.json(Tea));
|
|
}
|
|
|
|
{
|
|
// parses json
|
|
const r = testParse("POST / HTTP/1.0\r\nContent-Length: 17\r\n\r\n{\"type\":\"keemun\"}", .{});
|
|
defer testCleanup(r);
|
|
try t.expectString("keemun", (try r.json(Tea)).?.type);
|
|
try t.expectString("keemun", (try r.json(Tea)).?.type);
|
|
}
|
|
}
|
|
|
|
test "body: jsonValueTree" {
|
|
{
|
|
// too big
|
|
const r = testParse("POST / HTTP/1.0\r\nContent-Length: 17\r\n\r\n{\"type\":\"keemun\"}", .{.max_body_size = 16});
|
|
defer testCleanup(r);
|
|
try t.expectError(Error.BodyTooBig, r.jsonValueTree());
|
|
}
|
|
|
|
{
|
|
// no body
|
|
const r = testParse("PUT / HTTP/1.0\r\nHost: pondzpondz.com\r\nContent-Length: 0\r\n\r\n", .{.max_body_size = 10});
|
|
defer testCleanup(r);
|
|
try t.expectEqual(@as(?std.json.ValueTree, null), try r.jsonValueTree());
|
|
try t.expectEqual(@as(?std.json.ValueTree, null), try r.jsonValueTree());
|
|
}
|
|
|
|
{
|
|
// parses json
|
|
const r = testParse("POST / HTTP/1.0\r\nContent-Length: 17\r\n\r\n{\"type\":\"keemun\"}", .{});
|
|
defer testCleanup(r);
|
|
try t.expectString("keemun", (try r.jsonValueTree()).?.root.object.get("type").?.string);
|
|
try t.expectString("keemun", (try r.jsonValueTree()).?.root.object.get("type").?.string);
|
|
}
|
|
}
|
|
|
|
test "body: jsonObject" {
|
|
{
|
|
// too big
|
|
const r = testParse("POST / HTTP/1.0\r\nContent-Length: 17\r\n\r\n{\"type\":\"keemun\"}", .{.max_body_size = 16});
|
|
defer testCleanup(r);
|
|
try t.expectError(Error.BodyTooBig, r.jsonObject());
|
|
}
|
|
|
|
{
|
|
// no body
|
|
const r = testParse("PUT / HTTP/1.0\r\nHost: pondzpondz.com\r\nContent-Length: 0\r\n\r\n", .{.max_body_size = 10});
|
|
defer testCleanup(r);
|
|
try t.expectEqual(@as(?std.json.ObjectMap, null), try r.jsonObject());
|
|
try t.expectEqual(@as(?std.json.ObjectMap, null), try r.jsonObject());
|
|
}
|
|
|
|
{
|
|
// not an object
|
|
const r = testParse("POST / HTTP/1.0\r\nContent-Length: 7\r\n\r\n\"hello\"", .{});
|
|
defer testCleanup(r);
|
|
try t.expectEqual(@as(?std.json.ObjectMap, null), try r.jsonObject());
|
|
try t.expectEqual(@as(?std.json.ObjectMap, null), try r.jsonObject());
|
|
}
|
|
|
|
{
|
|
// parses json
|
|
const r = testParse("POST / HTTP/1.0\r\nContent-Length: 17\r\n\r\n{\"type\":\"keemun\"}", .{});
|
|
defer testCleanup(r);
|
|
try t.expectString("keemun", (try r.jsonObject()).?.get("type").?.string);
|
|
try t.expectString("keemun", (try r.jsonObject()).?.get("type").?.string);
|
|
}
|
|
}
|
|
|
|
// our t.Stream already simulates random TCP fragmentation.
|
|
test "request: fuzz" {
|
|
// We have a bunch of data to allocate for testing, like header names and
|
|
// values. Easier to use this arena and reset it after each test run.
|
|
var arena = std.heap.ArenaAllocator.init(t.allocator);
|
|
const aa = arena.allocator();
|
|
defer arena.deinit();
|
|
|
|
var r = t.getRandom();
|
|
const random = r.random();
|
|
for (0..1) |_| {
|
|
|
|
// important to test with different buffer sizes, since there's a lot of
|
|
// special handling for different cases (e.g the buffer is full and has
|
|
// some of the body in it, so we need to copy that to a dynamically allocated
|
|
// buffer)
|
|
|
|
// how many requests should we make on this 1 individual socket (simulating
|
|
// keepalive AND the request pool)
|
|
const buffer_size = random.uintAtMost(u16, 1024) + 1024;
|
|
var request = testRequest(.{.buffer_size = buffer_size});
|
|
defer {
|
|
// normally the arena
|
|
t.reset();
|
|
request.deinit(t.allocator);
|
|
t.allocator.destroy(request);
|
|
}
|
|
|
|
var s = t.Stream.init();
|
|
defer s.deinit();
|
|
|
|
request.stream = s;
|
|
request.arena = t.arena;
|
|
const number_of_requests = random.uintAtMost(u8, 10) + 1;
|
|
for (0..number_of_requests) |_| {
|
|
_ = arena.reset(.retain_capacity);
|
|
const method = randomMethod(random);
|
|
const url = t.randomString(random, aa, 20);
|
|
|
|
_ = s.add(method);
|
|
_ = s.add(" /");
|
|
_ = s.add(url);
|
|
|
|
const number_of_qs = random.uintAtMost(u8, 4);
|
|
if (number_of_qs != 0) {
|
|
_ = s.add("?");
|
|
}
|
|
var query = std.StringHashMap([]const u8).init(aa);
|
|
for (0..number_of_qs) |_| {
|
|
const key = t.randomString(random, aa, 20);
|
|
const value = t.randomString(random, aa, 20);
|
|
if (!query.contains(key)) {
|
|
// TODO: figure out how we want to handle duplicate query values
|
|
// (the spec doesn't specifiy what to do)
|
|
query.put(key, value) catch unreachable;
|
|
_ = s.add(key);
|
|
_ = s.add("=");
|
|
_ = s.add(value);
|
|
_ = s.add("&");
|
|
}
|
|
}
|
|
|
|
_ = s.add(" HTTP/1.1\r\n");
|
|
|
|
var headers = std.StringHashMap([]const u8).init(aa);
|
|
for (0..random.uintAtMost(u8, 4)) |_| {
|
|
const name = t.randomString(random, aa, 20);
|
|
const value = t.randomString(random, aa, 20);
|
|
if (!headers.contains(name)) {
|
|
// TODO: figure out how we want to handle duplicate query values
|
|
// Note, the spec says we should merge these!
|
|
headers.put(name, value) catch unreachable;
|
|
_ = s.add(name);
|
|
_ = s.add(": ");
|
|
_ = s.add(value);
|
|
_ = s.add("\r\n");
|
|
}
|
|
}
|
|
|
|
var body: ?[]u8 = null;
|
|
if (random.uintAtMost(u8, 4) == 0) {
|
|
_ = s.add("\r\n"); // no body
|
|
} else {
|
|
body = t.randomString(random, aa, 8000);
|
|
const cl = std.fmt.allocPrint(aa, "{d}", .{body.?.len}) catch unreachable;
|
|
headers.put("content-length", cl) catch unreachable;
|
|
_ = s.add("content-length: ");
|
|
_ = s.add(cl);
|
|
_ = s.add("\r\n\r\n");
|
|
_ = s.add(body.?);
|
|
}
|
|
|
|
request.parse() catch |err| {
|
|
std.debug.print("\nParse Error: {}\nInput: {s}", .{err, s.to_read.items[s.read_index..]});
|
|
unreachable;
|
|
};
|
|
|
|
// assert the headers
|
|
var it = headers.iterator();
|
|
while (it.next()) |entry| {
|
|
try t.expectString(entry.value_ptr.*, request.header(entry.key_ptr.*).?);
|
|
}
|
|
|
|
// assert the querystring
|
|
var actualQuery = request.query() catch unreachable;
|
|
it = query.iterator();
|
|
while (it.next()) |entry| {
|
|
try t.expectString(entry.value_ptr.*, actualQuery.get(entry.key_ptr.*).?);
|
|
}
|
|
|
|
// We dont' read the body by defalt. We donly read the body when the app
|
|
// calls req.body(). It's important that we test both cases. When the body
|
|
// isn't read, we still need to drain the bytes from the socket for when
|
|
// the socket is reused.
|
|
if (random.uintAtMost(u8, 4) != 0) {
|
|
var actual = request.body() catch unreachable;
|
|
if (body) |b| {
|
|
try t.expectString(b, actual.?);
|
|
} else {
|
|
try t.expectEqual(@as(?[]const u8, null), actual);
|
|
}
|
|
}
|
|
|
|
request.drain() catch unreachable;
|
|
request.reset();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn testParse(input: []const u8, config: Config) *Request {
|
|
var s = t.Stream.init();
|
|
_ = s.add(input);
|
|
|
|
var request = testRequest(config);
|
|
request.stream = s;
|
|
request.arena = t.arena;
|
|
request.parse() catch |err| {
|
|
testCleanup(request);
|
|
std.debug.print("\nParse Error: {}\nInput: {s}", .{err, input});
|
|
unreachable;
|
|
};
|
|
return request;
|
|
}
|
|
|
|
fn expectParseError(expected: Error, input: []const u8, config: Config) !void {
|
|
var s = t.Stream.init();
|
|
_ = s.add(input);
|
|
|
|
var request = testRequest(config);
|
|
defer testCleanup(request);
|
|
request.stream = s;
|
|
try t.expectError(expected, request.parse());
|
|
}
|
|
|
|
fn testRequest(config: Config, ) *Request {
|
|
var req = t.allocator.create(Request) catch unreachable;
|
|
req.init(t.allocator, t.allocator, config) catch unreachable;
|
|
return req;
|
|
}
|
|
|
|
fn testCleanup(r: *Request) void {
|
|
r.stream.deinit();
|
|
t.reset();
|
|
r.deinit(t.allocator);
|
|
t.allocator.destroy(r);
|
|
}
|
|
|
|
fn randomMethod(random: std.rand.Random) []const u8 {
|
|
return switch (random.uintAtMost(usize, 6)) {
|
|
0 => "GET",
|
|
1 => "PUT",
|
|
2 => "POST",
|
|
3 => "PATCH",
|
|
4 => "DELETE",
|
|
5 => "OPTIONS",
|
|
6 => "HEAD",
|
|
else => unreachable,
|
|
};
|
|
}
|