aboutsummaryrefslogtreecommitdiff
path: root/lib/std/http/protocol.zig
diff options
context:
space:
mode:
authorAndrew Kelley <andrew@ziglang.org>2024-02-11 17:17:09 -0700
committerAndrew Kelley <andrew@ziglang.org>2024-02-23 02:37:10 -0700
commit90bd4f226e2ba03634d31c73df06bf0a90fa0231 (patch)
tree0a5cc52fa31ac0e0d9f8f40bce9534c67c4bd89c /lib/std/http/protocol.zig
parentf1cf300c8fa9842ec9c812310bdc9f3aeeb75359 (diff)
downloadzig-90bd4f226e2ba03634d31c73df06bf0a90fa0231.tar.gz
zig-90bd4f226e2ba03634d31c73df06bf0a90fa0231.zip
std.http: remove the ability to heap-allocate headers
The buffer for HTTP headers is now always provided via a static buffer. As a consequence, OutOfMemory is no longer a member of the read() error set, and the API and implementation of Client and Server are simplified. error.HttpHeadersExceededSizeLimit is renamed to error.HttpHeadersOversize.
Diffstat (limited to 'lib/std/http/protocol.zig')
-rw-r--r--lib/std/http/protocol.zig144
1 files changed, 70 insertions, 74 deletions
diff --git a/lib/std/http/protocol.zig b/lib/std/http/protocol.zig
index 0ccafd2ee5..0caa4211cd 100644
--- a/lib/std/http/protocol.zig
+++ b/lib/std/http/protocol.zig
@@ -34,54 +34,49 @@ pub const State = enum {
pub const HeadersParser = struct {
state: State = .start,
- /// Whether or not `header_bytes` is allocated or was provided as a fixed buffer.
- header_bytes_owned: bool,
- /// Either a fixed buffer of len `max_header_bytes` or a dynamic buffer that can grow up to `max_header_bytes`.
+ /// A fixed buffer of len `max_header_bytes`.
/// Pointers into this buffer are not stable until after a message is complete.
- header_bytes: std.ArrayListUnmanaged(u8),
- /// The maximum allowed size of `header_bytes`.
- max_header_bytes: usize,
- next_chunk_length: u64 = 0,
+ header_bytes_buffer: []u8,
+ header_bytes_len: u32,
+ next_chunk_length: u64,
/// Whether this parser is done parsing a complete message.
/// A message is only done when the entire payload has been read.
- done: bool = false,
+ done: bool,
- /// Initializes the parser with a dynamically growing header buffer of up to `max` bytes.
- pub fn initDynamic(max: usize) HeadersParser {
+ /// Initializes the parser with a provided buffer `buf`.
+ pub fn init(buf: []u8) HeadersParser {
return .{
- .header_bytes = .{},
- .max_header_bytes = max,
- .header_bytes_owned = true,
+ .header_bytes_buffer = buf,
+ .header_bytes_len = 0,
+ .done = false,
+ .next_chunk_length = 0,
};
}
- /// Initializes the parser with a provided buffer `buf`.
- pub fn initStatic(buf: []u8) HeadersParser {
- return .{
- .header_bytes = .{ .items = buf[0..0], .capacity = buf.len },
- .max_header_bytes = buf.len,
- .header_bytes_owned = false,
+ /// Reinitialize the parser.
+ /// Asserts the parser is in the "done" state.
+ pub fn reset(hp: *HeadersParser) void {
+ assert(hp.done);
+ hp.* = .{
+ .state = .start,
+ .header_bytes_buffer = hp.header_bytes_buffer,
+ .header_bytes_len = 0,
+ .done = false,
+ .next_chunk_length = 0,
};
}
- /// Completely resets the parser to it's initial state.
- /// This must be called after a message is complete.
- pub fn reset(r: *HeadersParser) void {
- assert(r.done); // The message must be completely read before reset, otherwise the parser is in an invalid state.
-
- r.header_bytes.clearRetainingCapacity();
-
- r.* = .{
- .header_bytes = r.header_bytes,
- .max_header_bytes = r.max_header_bytes,
- .header_bytes_owned = r.header_bytes_owned,
- };
+ pub fn get(hp: HeadersParser) []u8 {
+ return hp.header_bytes_buffer[0..hp.header_bytes_len];
}
- /// Returns the number of bytes consumed by headers. This is always less than or equal to `bytes.len`.
- /// You should check `r.state.isContent()` after this to check if the headers are done.
+ /// Returns the number of bytes consumed by headers. This is always less
+ /// than or equal to `bytes.len`.
+ /// You should check `r.state.isContent()` after this to check if the
+ /// headers are done.
///
- /// If the amount returned is less than `bytes.len`, you may assume that the parser is in a content state and the
+ /// If the amount returned is less than `bytes.len`, you may assume that
+ /// the parser is in a content state and the
/// first byte of content is located at `bytes[result]`.
pub fn findHeadersEnd(r: *HeadersParser, bytes: []const u8) u32 {
const vector_len: comptime_int = @max(std.simd.suggestVectorLength(u8) orelse 1, 8);
@@ -410,11 +405,14 @@ pub const HeadersParser = struct {
}
}
- /// Returns the number of bytes consumed by the chunk size. This is always less than or equal to `bytes.len`.
- /// You should check `r.state == .chunk_data` after this to check if the chunk size has been fully parsed.
+ /// Returns the number of bytes consumed by the chunk size. This is always
+ /// less than or equal to `bytes.len`.
+ /// You should check `r.state == .chunk_data` after this to check if the
+ /// chunk size has been fully parsed.
///
- /// If the amount returned is less than `bytes.len`, you may assume that the parser is in the `chunk_data` state
- /// and that the first byte of the chunk is at `bytes[result]`.
+ /// If the amount returned is less than `bytes.len`, you may assume that
+ /// the parser is in the `chunk_data` state and that the first byte of the
+ /// chunk is at `bytes[result]`.
pub fn findChunkedLen(r: *HeadersParser, bytes: []const u8) u32 {
const len = @as(u32, @intCast(bytes.len));
@@ -488,30 +486,27 @@ pub const HeadersParser = struct {
return len;
}
- /// Returns whether or not the parser has finished parsing a complete message. A message is only complete after the
- /// entire body has been read and any trailing headers have been parsed.
+ /// Returns whether or not the parser has finished parsing a complete
+ /// message. A message is only complete after the entire body has been read
+ /// and any trailing headers have been parsed.
pub fn isComplete(r: *HeadersParser) bool {
return r.done and r.state == .finished;
}
- pub const CheckCompleteHeadError = mem.Allocator.Error || error{HttpHeadersExceededSizeLimit};
+ pub const CheckCompleteHeadError = error{HttpHeadersOversize};
- /// Pushes `in` into the parser. Returns the number of bytes consumed by the header. Any header bytes are appended
- /// to the `header_bytes` buffer.
- ///
- /// This function only uses `allocator` if `r.header_bytes_owned` is true, and may be undefined otherwise.
- pub fn checkCompleteHead(r: *HeadersParser, allocator: std.mem.Allocator, in: []const u8) CheckCompleteHeadError!u32 {
- if (r.state.isContent()) return 0;
+ /// Pushes `in` into the parser. Returns the number of bytes consumed by
+ /// the header. Any header bytes are appended to `header_bytes_buffer`.
+ pub fn checkCompleteHead(hp: *HeadersParser, in: []const u8) CheckCompleteHeadError!u32 {
+ if (hp.state.isContent()) return 0;
- const i = r.findHeadersEnd(in);
+ const i = hp.findHeadersEnd(in);
const data = in[0..i];
- if (r.header_bytes.items.len + data.len > r.max_header_bytes) {
- return error.HttpHeadersExceededSizeLimit;
- } else {
- if (r.header_bytes_owned) try r.header_bytes.ensureUnusedCapacity(allocator, data.len);
+ if (hp.header_bytes_len + data.len > hp.header_bytes_buffer.len)
+ return error.HttpHeadersOversize;
- r.header_bytes.appendSliceAssumeCapacity(data);
- }
+ @memcpy(hp.header_bytes_buffer[hp.header_bytes_len..][0..data.len], data);
+ hp.header_bytes_len += @intCast(data.len);
return i;
}
@@ -520,7 +515,8 @@ pub const HeadersParser = struct {
HttpChunkInvalid,
};
- /// Reads the body of the message into `buffer`. Returns the number of bytes placed in the buffer.
+ /// Reads the body of the message into `buffer`. Returns the number of
+ /// bytes placed in the buffer.
///
/// If `skip` is true, the buffer will be unused and the body will be skipped.
///
@@ -718,7 +714,7 @@ test "HeadersParser.findHeadersEnd" {
const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\nHello";
for (0..36) |i| {
- r = HeadersParser.initDynamic(0);
+ r = HeadersParser.init(&.{});
try std.testing.expectEqual(@as(u32, @intCast(i)), r.findHeadersEnd(data[0..i]));
try std.testing.expectEqual(@as(u32, @intCast(35 - i)), r.findHeadersEnd(data[i..]));
}
@@ -728,7 +724,7 @@ test "HeadersParser.findChunkedLen" {
var r: HeadersParser = undefined;
const data = "Ff\r\nf0f000 ; ext\n0\r\nffffffffffffffffffffffffffffffffffffffff\r\n";
- r = HeadersParser.initDynamic(0);
+ r = HeadersParser.init(&.{});
r.state = .chunk_head_size;
r.next_chunk_length = 0;
@@ -761,9 +757,9 @@ test "HeadersParser.findChunkedLen" {
test "HeadersParser.read length" {
// mock BufferedConnection for read
+ var headers_buf: [256]u8 = undefined;
- var r = HeadersParser.initDynamic(256);
- defer r.header_bytes.deinit(std.testing.allocator);
+ var r = HeadersParser.init(&headers_buf);
const data = "GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\nHello";
var conn: MockBufferedConnection = .{
@@ -773,8 +769,8 @@ test "HeadersParser.read length" {
while (true) { // read headers
try conn.fill();
- const nchecked = try r.checkCompleteHead(std.testing.allocator, conn.peek());
- conn.drop(@as(u16, @intCast(nchecked)));
+ const nchecked = try r.checkCompleteHead(conn.peek());
+ conn.drop(@intCast(nchecked));
if (r.state.isContent()) break;
}
@@ -786,14 +782,14 @@ test "HeadersParser.read length" {
try std.testing.expectEqual(@as(usize, 5), len);
try std.testing.expectEqualStrings("Hello", buf[0..len]);
- try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\n", r.header_bytes.items);
+ try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\n", r.get());
}
test "HeadersParser.read chunked" {
// mock BufferedConnection for read
- var r = HeadersParser.initDynamic(256);
- defer r.header_bytes.deinit(std.testing.allocator);
+ var headers_buf: [256]u8 = undefined;
+ var r = HeadersParser.init(&headers_buf);
const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n2\r\nHe\r\n2\r\nll\r\n1\r\no\r\n0\r\n\r\n";
var conn: MockBufferedConnection = .{
@@ -803,8 +799,8 @@ test "HeadersParser.read chunked" {
while (true) { // read headers
try conn.fill();
- const nchecked = try r.checkCompleteHead(std.testing.allocator, conn.peek());
- conn.drop(@as(u16, @intCast(nchecked)));
+ const nchecked = try r.checkCompleteHead(conn.peek());
+ conn.drop(@intCast(nchecked));
if (r.state.isContent()) break;
}
@@ -815,14 +811,14 @@ test "HeadersParser.read chunked" {
try std.testing.expectEqual(@as(usize, 5), len);
try std.testing.expectEqualStrings("Hello", buf[0..len]);
- try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n", r.header_bytes.items);
+ try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n", r.get());
}
test "HeadersParser.read chunked trailer" {
// mock BufferedConnection for read
- var r = HeadersParser.initDynamic(256);
- defer r.header_bytes.deinit(std.testing.allocator);
+ var headers_buf: [256]u8 = undefined;
+ var r = HeadersParser.init(&headers_buf);
const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n2\r\nHe\r\n2\r\nll\r\n1\r\no\r\n0\r\nContent-Type: text/plain\r\n\r\n";
var conn: MockBufferedConnection = .{
@@ -832,8 +828,8 @@ test "HeadersParser.read chunked trailer" {
while (true) { // read headers
try conn.fill();
- const nchecked = try r.checkCompleteHead(std.testing.allocator, conn.peek());
- conn.drop(@as(u16, @intCast(nchecked)));
+ const nchecked = try r.checkCompleteHead(conn.peek());
+ conn.drop(@intCast(nchecked));
if (r.state.isContent()) break;
}
@@ -847,11 +843,11 @@ test "HeadersParser.read chunked trailer" {
while (true) { // read headers
try conn.fill();
- const nchecked = try r.checkCompleteHead(std.testing.allocator, conn.peek());
- conn.drop(@as(u16, @intCast(nchecked)));
+ const nchecked = try r.checkCompleteHead(conn.peek());
+ conn.drop(@intCast(nchecked));
if (r.state.isContent()) break;
}
- try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\n\r\nContent-Type: text/plain\r\n\r\n", r.header_bytes.items);
+ try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\n\r\nContent-Type: text/plain\r\n\r\n", r.get());
}