Skip to content

Commit 5d94295

Browse files
committed
std.http.Headers.Parser: parse version and status
1 parent 2cdc0a8 commit 5d94295

3 files changed

Lines changed: 205 additions & 53 deletions

File tree

lib/std/http.zig

Lines changed: 6 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
pub const Client = @import("http/Client.zig");
2+
pub const Headers = @import("http/Headers.zig");
3+
4+
pub 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

245-
pub 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-
294251
const std = @import("std.zig");
295252

296253
test {

lib/std/http/Client.zig

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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

99102
pub 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

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
status: http.Status,
2+
version: http.Version,
3+
4+
pub 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+
163+
const std = @import("../std.zig");
164+
const http = std.http;
165+
const Headers = @This();
166+
const testing = std.testing;
167+
168+
test "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+
177+
test "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+
186+
test "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+
}

0 commit comments

Comments
 (0)