authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-01-03 19:14:44-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-01-04 18:37:53-07:00
log5d9429579dbab98d5d565e291dfe89f9b8261526
tree7bfc44fa57be7906f6376aebe1ae37ba861a575a
parent2cdc0a8b504a501d3eb5184f096720e6a5dc351a

std.http.Headers.Parser: parse version and status


3 files changed, 205 insertions(+), 53 deletions(-)

lib/std/http.zig+6-49
......@@ -1,4 +1,10 @@
11pub const Client = @import("http/Client.zig");
2pub const Headers = @import("http/Headers.zig");
3
4pub const Version = enum {
5 @"HTTP/1.0",
6 @"HTTP/1.1",
7};
28
39/// https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
410/// https://datatracker.ietf.org/doc/html/rfc7231#section-4 Initial definiton
......@@ -242,55 +248,6 @@ pub const Status = enum(u10) {
242248 }
243249};
244250
245pub const Headers = struct {
246 state: State = .start,
247 invalid_index: u32 = undefined,
248
249 pub const State = enum { invalid, start, line, nl_r, nl_n, nl2_r, finished };
250
251 /// Returns how many bytes are processed into headers. Always less than or
252 /// equal to bytes.len. If the amount returned is less than bytes.len, it
253 /// means the headers ended and the first byte after the double \r\n\r\n is
254 /// located at `bytes[result]`.
255 pub fn feed(h: *Headers, bytes: []const u8) usize {
256 for (bytes) |b, i| {
257 switch (h.state) {
258 .start => switch (b) {
259 '\r' => h.state = .nl_r,
260 '\n' => return invalid(h, i),
261 else => {},
262 },
263 .nl_r => switch (b) {
264 '\n' => h.state = .nl_n,
265 else => return invalid(h, i),
266 },
267 .nl_n => switch (b) {
268 '\r' => h.state = .nl2_r,
269 else => h.state = .line,
270 },
271 .nl2_r => switch (b) {
272 '\n' => h.state = .finished,
273 else => return invalid(h, i),
274 },
275 .line => switch (b) {
276 '\r' => h.state = .nl_r,
277 '\n' => return invalid(h, i),
278 else => {},
279 },
280 .invalid => return i,
281 .finished => return i,
282 }
283 }
284 return bytes.len;
285 }
286
287 fn invalid(h: *Headers, i: usize) usize {
288 h.invalid_index = @intCast(u32, i);
289 h.state = .invalid;
290 return i;
291 }
292};
293
294251const std = @import("std.zig");
295252
296253test {
lib/std/http/Client.zig+6-4
......@@ -21,7 +21,8 @@ pub const Request = struct {
2121 stream: net.Stream,
2222 tls_client: std.crypto.tls.Client,
2323 protocol: Protocol,
24 response_headers: http.Headers = .{},
24 response_headers: http.Headers,
25 redirects_left: u32,
2526
2627 pub const Headers = struct {
2728 method: http.Method = .GET,
......@@ -35,7 +36,9 @@ pub const Request = struct {
3536
3637 pub const Protocol = enum { http, https };
3738
38 pub const Options = struct {};
39 pub const Options = struct {
40 max_redirects: u32 = 3,
41 };
3942
4043 pub fn readAll(req: *Request, buffer: []u8) !usize {
4144 return readAtLeast(req, buffer, buffer.len);
......@@ -97,8 +100,6 @@ pub fn deinit(client: *Client, gpa: std.mem.Allocator) void {
97100}
98101
99102pub fn request(client: *Client, url: Url, headers: Request.Headers, options: Request.Options) !Request {
100 _ = options; // we have no options yet
101
102103 const protocol = std.meta.stringToEnum(Request.Protocol, url.scheme) orelse
103104 return error.UnsupportedUrlScheme;
104105 const port: u16 = url.port orelse switch (protocol) {
......@@ -111,6 +112,7 @@ pub fn request(client: *Client, url: Url, headers: Request.Headers, options: Req
111112 .stream = try net.tcpConnectToHost(client.allocator, url.host, port),
112113 .protocol = protocol,
113114 .tls_client = undefined,
115 .redirects_left = options.max_redirects,
114116 };
115117
116118 switch (protocol) {
lib/std/http/Headers.zig created+193
......@@ -0,0 +1,193 @@
1status: http.Status,
2version: http.Version,
3
4pub const Parser = struct {
5 state: State,
6 headers: Headers,
7 buffer: [16]u8,
8 buffer_index: u4,
9
10 pub const init: Parser = .{
11 .state = .start,
12 .headers = .{
13 .status = undefined,
14 .version = undefined,
15 },
16 .buffer = undefined,
17 .buffer_index = 0,
18 };
19
20 pub const State = enum {
21 invalid,
22 finished,
23 start,
24 expect_status,
25 find_start_line_end,
26 line,
27 line_r,
28 };
29
30 /// Returns how many bytes are processed into headers. Always less than or
31 /// equal to bytes.len. If the amount returned is less than bytes.len, it
32 /// means the headers ended and the first byte after the double \r\n\r\n is
33 /// located at `bytes[result]`.
34 pub fn feed(p: *Parser, bytes: []const u8) usize {
35 var index: usize = 0;
36
37 while (bytes.len - index >= 16) {
38 index += p.feed16(bytes[index..][0..16]);
39 switch (p.state) {
40 .invalid, .finished => return index,
41 else => continue,
42 }
43 }
44
45 while (index < bytes.len) {
46 var buffer = [1]u8{0} ** 16;
47 const src = bytes[index..bytes.len];
48 std.mem.copy(u8, &buffer, src);
49 index += p.feed16(&buffer);
50 switch (p.state) {
51 .invalid, .finished => return index,
52 else => continue,
53 }
54 }
55
56 return index;
57 }
58
59 pub fn feed16(p: *Parser, chunk: *const [16]u8) u8 {
60 switch (p.state) {
61 .invalid, .finished => return 0,
62 .start => {
63 p.headers.version = switch (std.mem.readIntNative(u64, chunk[0..8])) {
64 std.mem.readIntNative(u64, "HTTP/1.0") => .@"HTTP/1.0",
65 std.mem.readIntNative(u64, "HTTP/1.1") => .@"HTTP/1.1",
66 else => return invalid(p, 0),
67 };
68 p.state = .expect_status;
69 return 8;
70 },
71 .expect_status => {
72 // example: " 200 OK\r\n"
73 // example; " 301 Moved Permanently\r\n"
74 switch (std.mem.readIntNative(u64, chunk[0..8])) {
75 std.mem.readIntNative(u64, " 200 OK\r") => {
76 if (chunk[8] != '\n') return invalid(p, 8);
77 p.headers.status = .ok;
78 p.state = .line;
79 return 9;
80 },
81 std.mem.readIntNative(u64, " 301 Mov") => {
82 p.headers.status = .moved_permanently;
83 if (!std.mem.eql(u8, chunk[9..], "ed Perma"))
84 return invalid(p, 9);
85 p.state = .find_start_line_end;
86 return 16;
87 },
88 else => {
89 if (chunk[0] != ' ') return invalid(p, 0);
90 const status = std.fmt.parseInt(u10, chunk[1..][0..3], 10) catch
91 return invalid(p, 1);
92 p.headers.status = @intToEnum(http.Status, status);
93 const v: @Vector(12, u8) = chunk[4..16].*;
94 const matches_r = v == @splat(12, @as(u8, '\r'));
95 const iota = std.simd.iota(u8, 12);
96 const default = @splat(12, @as(u8, 12));
97 const index = 4 + @reduce(.Min, @select(u8, matches_r, iota, default));
98 if (index >= 15) {
99 p.state = .find_start_line_end;
100 return index;
101 }
102 if (chunk[index + 1] != '\n')
103 return invalid(p, index + 1);
104 p.state = .line;
105 return index + 2;
106 },
107 }
108 },
109 .find_start_line_end => {
110 const v: @Vector(16, u8) = chunk.*;
111 const matches_r = v == @splat(16, @as(u8, '\r'));
112 const iota = std.simd.iota(u8, 16);
113 const default = @splat(16, @as(u8, 16));
114 const index = @reduce(.Min, @select(u8, matches_r, iota, default));
115 if (index >= 15) {
116 p.state = .find_start_line_end;
117 return index;
118 }
119 if (chunk[index + 1] != '\n')
120 return invalid(p, index + 1);
121 p.state = .line;
122 return index + 2;
123 },
124 .line => {
125 const v: @Vector(16, u8) = chunk.*;
126 const matches_r = v == @splat(16, @as(u8, '\r'));
127 const iota = std.simd.iota(u8, 16);
128 const default = @splat(16, @as(u8, 16));
129 const index = @reduce(.Min, @select(u8, matches_r, iota, default));
130 if (index >= 15) {
131 return index;
132 }
133 if (chunk[index + 1] != '\n')
134 return invalid(p, index + 1);
135 if (index + 4 <= 16 and chunk[index + 2] == '\r') {
136 if (chunk[index + 3] != '\n') return invalid(p, index + 3);
137 p.state = .finished;
138 return index + 4;
139 }
140 p.state = .line_r;
141 return index + 2;
142 },
143 .line_r => {
144 if (chunk[0] == '\r') {
145 if (chunk[1] != '\n') return invalid(p, 1);
146 p.state = .finished;
147 return 2;
148 }
149 p.state = .line;
150 // Here would be nice to use this proposal when it is implemented:
151 // https://github.com/ziglang/zig/issues/8220
152 return 0;
153 },
154 }
155 }
156
157 fn invalid(p: *Parser, i: u8) u8 {
158 p.state = .invalid;
159 return i;
160 }
161};
162
163const std = @import("../std.zig");
164const http = std.http;
165const Headers = @This();
166const testing = std.testing;
167
168test "status line ok" {
169 var p = Parser.init;
170 const line = "HTTP/1.1 200 OK\r\n";
171 try testing.expect(p.feed(line) == line.len);
172 try testing.expectEqual(Parser.State.line, p.state);
173 try testing.expect(p.headers.version == .@"HTTP/1.1");
174 try testing.expect(p.headers.status == .ok);
175}
176
177test "status line non hot path long msg" {
178 var p = Parser.init;
179 const line = "HTTP/1.0 418 I'm a teapot\r\n";
180 try testing.expect(p.feed(line) == line.len);
181 try testing.expectEqual(Parser.State.line, p.state);
182 try testing.expect(p.headers.version == .@"HTTP/1.0");
183 try testing.expect(p.headers.status == .teapot);
184}
185
186test "status line non hot path short msg" {
187 var p = Parser.init;
188 const line = "HTTP/1.1 418 lol\r\n";
189 try testing.expect(p.feed(line) == line.len);
190 try testing.expectEqual(Parser.State.line, p.state);
191 try testing.expect(p.headers.version == .@"HTTP/1.1");
192 try testing.expect(p.headers.status == .teapot);
193}