Commit 2d090f61be

Andrew Kelley <andrew@ziglang.org>
2022-12-30 02:57:53
add std.http.Headers
This is a streaming HTTP header parser. All it currently does is detect the end of headers. This will be a non-allocating parser where one can bring supply their own buffer if they want to handle custom headers. This commit also improves std.http.Client to not return the HTTP headers with the read functions.
1 parent 05fee3b
Changed files (2)
lib
lib/std/http/Client.zig
@@ -16,6 +16,7 @@ pub const Request = struct {
     headers: std.ArrayListUnmanaged(u8) = .{},
     tls_client: std.crypto.tls.Client,
     protocol: Protocol,
+    response_headers: http.Headers = .{},
 
     pub const Protocol = enum { http, https };
 
@@ -51,18 +52,53 @@ pub const Request = struct {
         }
     }
 
+    pub fn readAll(req: *Request, buffer: []u8) !usize {
+        return readAtLeast(req, buffer, buffer.len);
+    }
+
     pub fn read(req: *Request, buffer: []u8) !usize {
+        return readAtLeast(req, buffer, 1);
+    }
+
+    pub fn readAtLeast(req: *Request, buffer: []u8, len: usize) !usize {
+        assert(len <= buffer.len);
+        var index: usize = 0;
+        while (index < len) {
+            const headers_finished = req.response_headers.state == .finished;
+            const amt = try readAdvanced(req, buffer[index..]);
+            if (amt == 0 and headers_finished) break;
+            index += amt;
+        }
+        return index;
+    }
+
+    /// This one can return 0 without meaning EOF.
+    /// TODO change to readvAdvanced
+    pub fn readAdvanced(req: *Request, buffer: []u8) !usize {
+        if (req.response_headers.state == .finished) return readRaw(req, buffer);
+
+        const amt = try readRaw(req, buffer);
+        const data = buffer[0..amt];
+        const i = req.response_headers.feed(data);
+        if (req.response_headers.state == .invalid) return error.InvalidHttpHeaders;
+        if (i < data.len) {
+            const rest = data[i..];
+            std.mem.copy(u8, buffer, rest);
+            return rest.len;
+        }
+        return 0;
+    }
+
+    /// Only abstracts over http/https.
+    fn readRaw(req: *Request, buffer: []u8) !usize {
         switch (req.protocol) {
             .http => return req.stream.read(buffer),
             .https => return req.tls_client.read(req.stream, buffer),
         }
     }
 
-    pub fn readAll(req: *Request, buffer: []u8) !usize {
-        return readAtLeast(req, buffer, buffer.len);
-    }
-
-    pub fn readAtLeast(req: *Request, buffer: []u8, len: usize) !usize {
+    /// Only abstracts over http/https.
+    fn readAtLeastRaw(req: *Request, buffer: []u8, len: usize) !usize {
         switch (req.protocol) {
             .http => return req.stream.readAtLeast(buffer, len),
             .https => return req.tls_client.readAtLeast(req.stream, buffer, len),
lib/std/http.zig
@@ -242,10 +242,60 @@ pub const Status = enum(u10) {
     }
 };
 
+pub const Headers = struct {
+    state: State = .start,
+    invalid_index: u32 = undefined,
+
+    pub const State = enum { invalid, start, line, nl_r, nl_n, nl2_r, finished };
+
+    /// Returns how many bytes are processed into headers. Always less than or
+    /// equal to bytes.len. If the amount returned is less than bytes.len, it
+    /// means the headers ended and the first byte after the double \r\n\r\n is
+    /// located at `bytes[result]`.
+    pub fn feed(h: *Headers, bytes: []const u8) usize {
+        for (bytes) |b, i| {
+            switch (h.state) {
+                .start => switch (b) {
+                    '\r' => h.state = .nl_r,
+                    '\n' => return invalid(h, i),
+                    else => {},
+                },
+                .nl_r => switch (b) {
+                    '\n' => h.state = .nl_n,
+                    else => return invalid(h, i),
+                },
+                .nl_n => switch (b) {
+                    '\r' => h.state = .nl2_r,
+                    else => h.state = .line,
+                },
+                .nl2_r => switch (b) {
+                    '\n' => h.state = .finished,
+                    else => return invalid(h, i),
+                },
+                .line => switch (b) {
+                    '\r' => h.state = .nl_r,
+                    '\n' => return invalid(h, i),
+                    else => {},
+                },
+                .invalid => return i,
+                .finished => return i,
+            }
+        }
+        return bytes.len;
+    }
+
+    fn invalid(h: *Headers, i: usize) usize {
+        h.invalid_index = @intCast(u32, i);
+        h.state = .invalid;
+        return i;
+    }
+};
+
 const std = @import("std.zig");
 
 test {
     _ = Client;
     _ = Method;
     _ = Status;
+    _ = Headers;
 }