authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-01-07 01:52:36-05:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2023-01-07 01:52:36-05:00
log87b223428a8953b6af9bea61ff4cc905821c0557
tree22e6f6a69d70e16fc525fdad522e16db074de384
parent0507ced8cd1bc40a8118adf7a7b00eb0cbd203dc
parentaa87789c29d2da1adb02eccd31e27c84c3dfec30
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

Merge pull request #14207 from MasterQ32/zig-uri-upstream

Ports zig-uri to stdlib.

4 files changed, 623 insertions(+), 136 deletions(-)

lib/std/Uri.zig created+512
...@@ -0,0 +1,512 @@
1//! Implements URI parsing roughly adhering to <https://tools.ietf.org/html/rfc3986>.
2//! Does not do perfect grammar and character class checking, but should be robust against URIs in the wild.
3
4const Uri = @This();
5const std = @import("std.zig");
6const testing = std.testing;
7
8scheme: []const u8,
9user: ?[]const u8,
10password: ?[]const u8,
11host: ?[]const u8,
12port: ?u16,
13path: []const u8,
14query: ?[]const u8,
15fragment: ?[]const u8,
16
17/// Applies URI encoding and replaces all reserved characters with their respective %XX code.
18pub fn escapeString(allocator: std.mem.Allocator, input: []const u8) error{OutOfMemory}![]const u8 {
19 var outsize: usize = 0;
20 for (input) |c| {
21 outsize += if (isUnreserved(c)) @as(usize, 1) else 3;
22 }
23 var output = try allocator.alloc(u8, outsize);
24 var outptr: usize = 0;
25
26 for (input) |c| {
27 if (isUnreserved(c)) {
28 output[outptr] = c;
29 outptr += 1;
30 } else {
31 var buf: [2]u8 = undefined;
32 _ = std.fmt.bufPrint(&buf, "{X:0>2}", .{c}) catch unreachable;
33
34 output[outptr + 0] = '%';
35 output[outptr + 1] = buf[0];
36 output[outptr + 2] = buf[1];
37 outptr += 3;
38 }
39 }
40 return output;
41}
42
43/// Parses a URI string and unescapes all %XX where XX is a valid hex number. Otherwise, verbatim copies
44/// them to the output.
45pub fn unescapeString(allocator: std.mem.Allocator, input: []const u8) error{OutOfMemory}![]const u8 {
46 var outsize: usize = 0;
47 var inptr: usize = 0;
48 while (inptr < input.len) {
49 if (input[inptr] == '%') {
50 inptr += 1;
51 if (inptr + 2 <= input.len) {
52 _ = std.fmt.parseInt(u8, input[inptr..][0..2], 16) catch {
53 outsize += 3;
54 inptr += 2;
55 continue;
56 };
57 inptr += 2;
58 outsize += 1;
59 }
60 } else {
61 inptr += 1;
62 outsize += 1;
63 }
64 }
65
66 var output = try allocator.alloc(u8, outsize);
67 var outptr: usize = 0;
68 inptr = 0;
69 while (inptr < input.len) {
70 if (input[inptr] == '%') {
71 inptr += 1;
72 if (inptr + 2 <= input.len) {
73 const value = std.fmt.parseInt(u8, input[inptr..][0..2], 16) catch {
74 output[outptr + 0] = input[inptr + 0];
75 output[outptr + 1] = input[inptr + 1];
76 inptr += 2;
77 outptr += 2;
78 continue;
79 };
80
81 output[outptr] = value;
82
83 inptr += 2;
84 outptr += 1;
85 }
86 } else {
87 output[outptr] = input[inptr];
88 inptr += 1;
89 outptr += 1;
90 }
91 }
92 return output;
93}
94
95pub const ParseError = error{ UnexpectedCharacter, InvalidFormat, InvalidPort };
96
97/// Parses the URI or returns an error.
98/// The return value will contain unescaped strings pointing into the
99/// original `text`. Each component that is provided, will be non-`null`.
100pub fn parse(text: []const u8) ParseError!Uri {
101 var reader = SliceReader{ .slice = text };
102 var uri = Uri{
103 .scheme = reader.readWhile(isSchemeChar),
104 .user = null,
105 .password = null,
106 .host = null,
107 .port = null,
108 .path = "", // path is always set, but empty by default.
109 .query = null,
110 .fragment = null,
111 };
112
113 // after the scheme, a ':' must appear
114 if (reader.get()) |c| {
115 if (c != ':')
116 return error.UnexpectedCharacter;
117 } else {
118 return error.InvalidFormat;
119 }
120
121 if (reader.peekPrefix("//")) { // authority part
122 std.debug.assert(reader.get().? == '/');
123 std.debug.assert(reader.get().? == '/');
124
125 const authority = reader.readUntil(isAuthoritySeparator);
126 if (authority.len == 0)
127 return error.InvalidFormat;
128
129 var start_of_host: usize = 0;
130 if (std.mem.indexOf(u8, authority, "@")) |index| {
131 start_of_host = index + 1;
132 const user_info = authority[0..index];
133
134 if (std.mem.indexOf(u8, user_info, ":")) |idx| {
135 uri.user = user_info[0..idx];
136 if (idx < user_info.len - 1) { // empty password is also "no password"
137 uri.password = user_info[idx + 1 ..];
138 }
139 } else {
140 uri.user = user_info;
141 uri.password = null;
142 }
143 }
144
145 var end_of_host: usize = authority.len;
146
147 if (authority[start_of_host] == '[') { // IPv6
148 end_of_host = std.mem.lastIndexOf(u8, authority, "]") orelse return error.InvalidFormat;
149 end_of_host += 1;
150
151 if (std.mem.lastIndexOf(u8, authority, ":")) |index| {
152 if (index >= end_of_host) { // if not part of the V6 address field
153 end_of_host = std.math.min(end_of_host, index);
154 uri.port = std.fmt.parseInt(u16, authority[index + 1 ..], 10) catch return error.InvalidPort;
155 }
156 }
157 } else if (std.mem.lastIndexOf(u8, authority, ":")) |index| {
158 if (index >= start_of_host) { // if not part of the userinfo field
159 end_of_host = std.math.min(end_of_host, index);
160 uri.port = std.fmt.parseInt(u16, authority[index + 1 ..], 10) catch return error.InvalidPort;
161 }
162 }
163
164 uri.host = authority[start_of_host..end_of_host];
165 }
166
167 uri.path = reader.readUntil(isPathSeparator);
168
169 if ((reader.peek() orelse 0) == '?') { // query part
170 std.debug.assert(reader.get().? == '?');
171 uri.query = reader.readUntil(isQuerySeparator);
172 }
173
174 if ((reader.peek() orelse 0) == '#') { // fragment part
175 std.debug.assert(reader.get().? == '#');
176 uri.fragment = reader.readUntilEof();
177 }
178
179 return uri;
180}
181
182const SliceReader = struct {
183 const Self = @This();
184
185 slice: []const u8,
186 offset: usize = 0,
187
188 fn get(self: *Self) ?u8 {
189 if (self.offset >= self.slice.len)
190 return null;
191 const c = self.slice[self.offset];
192 self.offset += 1;
193 return c;
194 }
195
196 fn peek(self: Self) ?u8 {
197 if (self.offset >= self.slice.len)
198 return null;
199 return self.slice[self.offset];
200 }
201
202 fn readWhile(self: *Self, comptime predicate: fn (u8) bool) []const u8 {
203 const start = self.offset;
204 var end = start;
205 while (end < self.slice.len and predicate(self.slice[end])) {
206 end += 1;
207 }
208 self.offset = end;
209 return self.slice[start..end];
210 }
211
212 fn readUntil(self: *Self, comptime predicate: fn (u8) bool) []const u8 {
213 const start = self.offset;
214 var end = start;
215 while (end < self.slice.len and !predicate(self.slice[end])) {
216 end += 1;
217 }
218 self.offset = end;
219 return self.slice[start..end];
220 }
221
222 fn readUntilEof(self: *Self) []const u8 {
223 const start = self.offset;
224 self.offset = self.slice.len;
225 return self.slice[start..];
226 }
227
228 fn peekPrefix(self: Self, prefix: []const u8) bool {
229 if (self.offset + prefix.len > self.slice.len)
230 return false;
231 return std.mem.eql(u8, self.slice[self.offset..][0..prefix.len], prefix);
232 }
233};
234
235/// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
236fn isSchemeChar(c: u8) bool {
237 return switch (c) {
238 'A'...'Z', 'a'...'z', '0'...'9', '+', '-', '.' => true,
239 else => false,
240 };
241}
242
243fn isAuthoritySeparator(c: u8) bool {
244 return switch (c) {
245 '/', '?', '#' => true,
246 else => false,
247 };
248}
249
250/// reserved = gen-delims / sub-delims
251fn isReserved(c: u8) bool {
252 return isGenLimit(c) or isSubLimit(c);
253}
254
255/// gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
256fn isGenLimit(c: u8) bool {
257 return switch (c) {
258 ':', ',', '?', '#', '[', ']', '@' => true,
259 else => false,
260 };
261}
262
263/// sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
264/// / "*" / "+" / "," / ";" / "="
265fn isSubLimit(c: u8) bool {
266 return switch (c) {
267 '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=' => true,
268 else => false,
269 };
270}
271
272/// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
273fn isUnreserved(c: u8) bool {
274 return switch (c) {
275 'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true,
276 else => false,
277 };
278}
279
280fn isPathSeparator(c: u8) bool {
281 return switch (c) {
282 '?', '#' => true,
283 else => false,
284 };
285}
286
287fn isQuerySeparator(c: u8) bool {
288 return switch (c) {
289 '#' => true,
290 else => false,
291 };
292}
293
294test "basic" {
295 const parsed = try parse("https://ziglang.org/download");
296 try testing.expectEqualStrings("https", parsed.scheme);
297 try testing.expectEqualStrings("ziglang.org", parsed.host orelse return error.UnexpectedNull);
298 try testing.expectEqualStrings("/download", parsed.path);
299 try testing.expectEqual(@as(?u16, null), parsed.port);
300}
301
302test "with port" {
303 const parsed = try parse("http://example:1337/");
304 try testing.expectEqualStrings("http", parsed.scheme);
305 try testing.expectEqualStrings("example", parsed.host orelse return error.UnexpectedNull);
306 try testing.expectEqualStrings("/", parsed.path);
307 try testing.expectEqual(@as(?u16, 1337), parsed.port);
308}
309
310test "should fail gracefully" {
311 try std.testing.expectEqual(@as(ParseError!Uri, error.InvalidFormat), parse("foobar://"));
312}
313
314test "scheme" {
315 try std.testing.expectEqualSlices(u8, "http", (try parse("http:_")).scheme);
316 try std.testing.expectEqualSlices(u8, "scheme-mee", (try parse("scheme-mee:_")).scheme);
317 try std.testing.expectEqualSlices(u8, "a.b.c", (try parse("a.b.c:_")).scheme);
318 try std.testing.expectEqualSlices(u8, "ab+", (try parse("ab+:_")).scheme);
319 try std.testing.expectEqualSlices(u8, "X+++", (try parse("X+++:_")).scheme);
320 try std.testing.expectEqualSlices(u8, "Y+-.", (try parse("Y+-.:_")).scheme);
321}
322
323test "authority" {
324 try std.testing.expectEqualSlices(u8, "hostname", (try parse("scheme://hostname")).host.?);
325
326 try std.testing.expectEqualSlices(u8, "hostname", (try parse("scheme://userinfo@hostname")).host.?);
327 try std.testing.expectEqualSlices(u8, "userinfo", (try parse("scheme://userinfo@hostname")).user.?);
328 try std.testing.expectEqual(@as(?[]const u8, null), (try parse("scheme://userinfo@hostname")).password);
329
330 try std.testing.expectEqualSlices(u8, "hostname", (try parse("scheme://user:password@hostname")).host.?);
331 try std.testing.expectEqualSlices(u8, "user", (try parse("scheme://user:password@hostname")).user.?);
332 try std.testing.expectEqualSlices(u8, "password", (try parse("scheme://user:password@hostname")).password.?);
333
334 try std.testing.expectEqualSlices(u8, "hostname", (try parse("scheme://hostname:0")).host.?);
335 try std.testing.expectEqual(@as(u16, 1234), (try parse("scheme://hostname:1234")).port.?);
336
337 try std.testing.expectEqualSlices(u8, "hostname", (try parse("scheme://userinfo@hostname:1234")).host.?);
338 try std.testing.expectEqual(@as(u16, 1234), (try parse("scheme://userinfo@hostname:1234")).port.?);
339 try std.testing.expectEqualSlices(u8, "userinfo", (try parse("scheme://userinfo@hostname:1234")).user.?);
340 try std.testing.expectEqual(@as(?[]const u8, null), (try parse("scheme://userinfo@hostname:1234")).password);
341
342 try std.testing.expectEqualSlices(u8, "hostname", (try parse("scheme://user:password@hostname:1234")).host.?);
343 try std.testing.expectEqual(@as(u16, 1234), (try parse("scheme://user:password@hostname:1234")).port.?);
344 try std.testing.expectEqualSlices(u8, "user", (try parse("scheme://user:password@hostname:1234")).user.?);
345 try std.testing.expectEqualSlices(u8, "password", (try parse("scheme://user:password@hostname:1234")).password.?);
346}
347
348test "authority.password" {
349 try std.testing.expectEqualSlices(u8, "username", (try parse("scheme://username@a")).user.?);
350 try std.testing.expectEqual(@as(?[]const u8, null), (try parse("scheme://username@a")).password);
351
352 try std.testing.expectEqualSlices(u8, "username", (try parse("scheme://username:@a")).user.?);
353 try std.testing.expectEqual(@as(?[]const u8, null), (try parse("scheme://username:@a")).password);
354
355 try std.testing.expectEqualSlices(u8, "username", (try parse("scheme://username:password@a")).user.?);
356 try std.testing.expectEqualSlices(u8, "password", (try parse("scheme://username:password@a")).password.?);
357
358 try std.testing.expectEqualSlices(u8, "username", (try parse("scheme://username::@a")).user.?);
359 try std.testing.expectEqualSlices(u8, ":", (try parse("scheme://username::@a")).password.?);
360}
361
362fn testAuthorityHost(comptime hostlist: anytype) !void {
363 inline for (hostlist) |hostname| {
364 try std.testing.expectEqualSlices(u8, hostname, (try parse("scheme://" ++ hostname)).host.?);
365 }
366}
367
368test "authority.dns-names" {
369 try testAuthorityHost(.{
370 "a",
371 "a.b",
372 "example.com",
373 "www.example.com",
374 "example.org.",
375 "www.example.org.",
376 "xn--nw2a.xn--j6w193g", // internationalized URI: 見.香港
377 "fe80--1ff-fe23-4567-890as3.ipv6-literal.net",
378 });
379}
380
381test "authority.IPv4" {
382 try testAuthorityHost(.{
383 "127.0.0.1",
384 "255.255.255.255",
385 "0.0.0.0",
386 "8.8.8.8",
387 "1.2.3.4",
388 "192.168.0.1",
389 "10.42.0.0",
390 });
391}
392
393test "authority.IPv6" {
394 try testAuthorityHost(.{
395 "[2001:db8:0:0:0:0:2:1]",
396 "[2001:db8::2:1]",
397 "[2001:db8:0000:1:1:1:1:1]",
398 "[2001:db8:0:1:1:1:1:1]",
399 "[0:0:0:0:0:0:0:0]",
400 "[0:0:0:0:0:0:0:1]",
401 "[::1]",
402 "[::]",
403 "[2001:db8:85a3:8d3:1319:8a2e:370:7348]",
404 "[fe80::1ff:fe23:4567:890a%25eth2]",
405 "[fe80::1ff:fe23:4567:890a]",
406 "[fe80::1ff:fe23:4567:890a%253]",
407 "[fe80:3::1ff:fe23:4567:890a]",
408 });
409}
410
411test "RFC example 1" {
412 const uri = "foo://example.com:8042/over/there?name=ferret#nose";
413 try std.testing.expectEqual(Uri{
414 .scheme = uri[0..3],
415 .user = null,
416 .password = null,
417 .host = uri[6..17],
418 .port = 8042,
419 .path = uri[22..33],
420 .query = uri[34..45],
421 .fragment = uri[46..50],
422 }, try parse(uri));
423}
424
425test "RFX example 2" {
426 const uri = "urn:example:animal:ferret:nose";
427 try std.testing.expectEqual(Uri{
428 .scheme = uri[0..3],
429 .user = null,
430 .password = null,
431 .host = null,
432 .port = null,
433 .path = uri[4..],
434 .query = null,
435 .fragment = null,
436 }, try parse(uri));
437}
438
439// source:
440// https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Examples
441test "Examples from wikipedia" {
442 const list = [_][]const u8{
443 "https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top",
444 "ldap://[2001:db8::7]/c=GB?objectClass?one",
445 "mailto:John.Doe@example.com",
446 "news:comp.infosystems.www.servers.unix",
447 "tel:+1-816-555-1212",
448 "telnet://192.0.2.16:80/",
449 "urn:oasis:names:specification:docbook:dtd:xml:4.1.2",
450 "http://a/b/c/d;p?q",
451 };
452 for (list) |uri| {
453 _ = try parse(uri);
454 }
455}
456
457// source:
458// https://tools.ietf.org/html/rfc3986#section-5.4.1
459test "Examples from RFC3986" {
460 const list = [_][]const u8{
461 "http://a/b/c/g",
462 "http://a/b/c/g",
463 "http://a/b/c/g/",
464 "http://a/g",
465 "http://g",
466 "http://a/b/c/d;p?y",
467 "http://a/b/c/g?y",
468 "http://a/b/c/d;p?q#s",
469 "http://a/b/c/g#s",
470 "http://a/b/c/g?y#s",
471 "http://a/b/c/;x",
472 "http://a/b/c/g;x",
473 "http://a/b/c/g;x?y#s",
474 "http://a/b/c/d;p?q",
475 "http://a/b/c/",
476 "http://a/b/c/",
477 "http://a/b/",
478 "http://a/b/",
479 "http://a/b/g",
480 "http://a/",
481 "http://a/",
482 "http://a/g",
483 };
484 for (list) |uri| {
485 _ = try parse(uri);
486 }
487}
488
489test "Special test" {
490 // This is for all of you code readers ♥
491 _ = try parse("https://www.youtube.com/watch?v=dQw4w9WgXcQ&feature=youtu.be&t=0");
492}
493
494test "URI escaping" {
495 const input = "\\ö/ äöß ~~.adas-https://canvas:123/#ads&&sad";
496 const expected = "%5C%C3%B6%2F%20%C3%A4%C3%B6%C3%9F%20~~.adas-https%3A%2F%2Fcanvas%3A123%2F%23ads%26%26sad";
497
498 const actual = try escapeString(std.testing.allocator, input);
499 defer std.testing.allocator.free(actual);
500
501 try std.testing.expectEqualSlices(u8, expected, actual);
502}
503
504test "URI unescaping" {
505 const input = "%5C%C3%B6%2F%20%C3%A4%C3%B6%C3%9F%20~~.adas-https%3A%2F%2Fcanvas%3A123%2F%23ads%26%26sad";
506 const expected = "\\ö/ äöß ~~.adas-https://canvas:123/#ads&&sad";
507
508 const actual = try unescapeString(std.testing.allocator, input);
509 defer std.testing.allocator.free(actual);
510
511 try std.testing.expectEqualSlices(u8, expected, actual);
512}
lib/std/Url.zig deleted-98
...@@ -1,98 +0,0 @@
1scheme: []const u8,
2host: []const u8,
3path: []const u8,
4port: ?u16,
5
6/// TODO: redo this implementation according to RFC 1738. This code is only a
7/// placeholder for now.
8pub fn parse(s: []const u8) !Url {
9 var scheme_end: usize = 0;
10 var host_start: usize = 0;
11 var host_end: usize = 0;
12 var path_start: usize = 0;
13 var port_start: usize = 0;
14 var port_end: usize = 0;
15 var state: enum {
16 scheme,
17 scheme_slash1,
18 scheme_slash2,
19 host,
20 port,
21 path,
22 } = .scheme;
23
24 for (s) |b, i| switch (state) {
25 .scheme => switch (b) {
26 ':' => {
27 state = .scheme_slash1;
28 scheme_end = i;
29 },
30 else => {},
31 },
32 .scheme_slash1 => switch (b) {
33 '/' => {
34 state = .scheme_slash2;
35 },
36 else => return error.InvalidUrl,
37 },
38 .scheme_slash2 => switch (b) {
39 '/' => {
40 state = .host;
41 host_start = i + 1;
42 },
43 else => return error.InvalidUrl,
44 },
45 .host => switch (b) {
46 ':' => {
47 state = .port;
48 host_end = i;
49 port_start = i + 1;
50 },
51 '/' => {
52 state = .path;
53 host_end = i;
54 path_start = i;
55 },
56 else => {},
57 },
58 .port => switch (b) {
59 '/' => {
60 port_end = i;
61 state = .path;
62 path_start = i;
63 },
64 else => {},
65 },
66 .path => {},
67 };
68
69 const port_slice = s[port_start..port_end];
70 const port = if (port_slice.len == 0) null else try std.fmt.parseInt(u16, port_slice, 10);
71
72 return .{
73 .scheme = s[0..scheme_end],
74 .host = s[host_start..host_end],
75 .path = s[path_start..],
76 .port = port,
77 };
78}
79
80const Url = @This();
81const std = @import("std.zig");
82const testing = std.testing;
83
84test "basic" {
85 const parsed = try parse("https://ziglang.org/download");
86 try testing.expectEqualStrings("https", parsed.scheme);
87 try testing.expectEqualStrings("ziglang.org", parsed.host);
88 try testing.expectEqualStrings("/download", parsed.path);
89 try testing.expectEqual(@as(?u16, null), parsed.port);
90}
91
92test "with port" {
93 const parsed = try parse("http://example:1337/");
94 try testing.expectEqualStrings("http", parsed.scheme);
95 try testing.expectEqualStrings("example", parsed.host);
96 try testing.expectEqualStrings("/", parsed.path);
97 try testing.expectEqual(@as(?u16, 1337), parsed.port);
98}
lib/std/http/Client.zig+110-37
...@@ -1,7 +1,3 @@...@@ -1,7 +1,3 @@
1//! This API is a barely-touched, barely-functional http client, just the
2//! absolute minimum thing I needed in order to test `std.crypto.tls`. Bear
3//! with me and I promise the API will become useful and streamlined.
4//!
5//! TODO: send connection: keep-alive and LRU cache a configurable number of1//! TODO: send connection: keep-alive and LRU cache a configurable number of
6//! open connections to skip DNS and TLS handshake for subsequent requests.2//! open connections to skip DNS and TLS handshake for subsequent requests.
73
...@@ -11,7 +7,7 @@ const assert = std.debug.assert;...@@ -11,7 +7,7 @@ const assert = std.debug.assert;
11const http = std.http;7const http = std.http;
12const net = std.net;8const net = std.net;
13const Client = @This();9const Client = @This();
14const Url = std.Url;10const Uri = std.Uri;
15const Allocator = std.mem.Allocator;11const Allocator = std.mem.Allocator;
16const testing = std.testing;12const testing = std.testing;
1713
...@@ -178,6 +174,8 @@ pub const Request = struct {...@@ -178,6 +174,8 @@ pub const Request = struct {
178 seen_rnr,174 seen_rnr,
179 finished,175 finished,
180 /// Begin transfer-encoding: chunked parsing states.176 /// Begin transfer-encoding: chunked parsing states.
177 chunk_size_prefix_r,
178 chunk_size_prefix_n,
181 chunk_size,179 chunk_size,
182 chunk_r,180 chunk_r,
183 chunk_data,181 chunk_data,
...@@ -382,6 +380,8 @@ pub const Request = struct {...@@ -382,6 +380,8 @@ pub const Request = struct {
382 continue :state;380 continue :state;
383 },381 },
384 },382 },
383 .chunk_size_prefix_r => unreachable,
384 .chunk_size_prefix_n => unreachable,
385 .chunk_size => unreachable,385 .chunk_size => unreachable,
386 .chunk_r => unreachable,386 .chunk_r => unreachable,
387 .chunk_data => unreachable,387 .chunk_data => unreachable,
...@@ -449,18 +449,6 @@ pub const Request = struct {...@@ -449,18 +449,6 @@ pub const Request = struct {
449 try expectEqual(@as(u10, 999), parseInt3("999".*));449 try expectEqual(@as(u10, 999), parseInt3("999".*));
450 }450 }
451451
452 inline fn int16(array: *const [2]u8) u16 {
453 return @bitCast(u16, array.*);
454 }
455
456 inline fn int32(array: *const [4]u8) u32 {
457 return @bitCast(u32, array.*);
458 }
459
460 inline fn int64(array: *const [8]u8) u64 {
461 return @bitCast(u64, array.*);
462 }
463
464 test "find headers end basic" {452 test "find headers end basic" {
465 var buffer: [1]u8 = undefined;453 var buffer: [1]u8 = undefined;
466 var r = Response.initStatic(&buffer);454 var r = Response.initStatic(&buffer);
...@@ -480,6 +468,29 @@ pub const Request = struct {...@@ -480,6 +468,29 @@ pub const Request = struct {
480 "\r\ncontent";468 "\r\ncontent";
481 try testing.expectEqual(@as(usize, 131), r.findHeadersEnd(example));469 try testing.expectEqual(@as(usize, 131), r.findHeadersEnd(example));
482 }470 }
471
472 test "find headers end bug" {
473 var buffer: [1]u8 = undefined;
474 var r = Response.initStatic(&buffer);
475 const trail = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
476 const example =
477 "HTTP/1.1 200 OK\r\n" ++
478 "Access-Control-Allow-Origin: https://render.githubusercontent.com\r\n" ++
479 "content-disposition: attachment; filename=zig-0.10.0.tar.gz\r\n" ++
480 "Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox\r\n" ++
481 "Content-Type: application/x-gzip\r\n" ++
482 "ETag: \"bfae0af6b01c7c0d89eb667cb5f0e65265968aeebda2689177e6b26acd3155ca\"\r\n" ++
483 "Strict-Transport-Security: max-age=31536000\r\n" ++
484 "Vary: Authorization,Accept-Encoding,Origin\r\n" ++
485 "X-Content-Type-Options: nosniff\r\n" ++
486 "X-Frame-Options: deny\r\n" ++
487 "X-XSS-Protection: 1; mode=block\r\n" ++
488 "Date: Fri, 06 Jan 2023 22:26:22 GMT\r\n" ++
489 "Transfer-Encoding: chunked\r\n" ++
490 "X-GitHub-Request-Id: 89C6:17E9:A7C9E:124B51:63B8A00E\r\n" ++
491 "connection: close\r\n\r\n" ++ trail;
492 try testing.expectEqual(@as(usize, example.len - trail.len), r.findHeadersEnd(example));
493 }
483 };494 };
484495
485 pub const Headers = struct {496 pub const Headers = struct {
...@@ -536,8 +547,7 @@ pub const Request = struct {...@@ -536,8 +547,7 @@ pub const Request = struct {
536 /// This one can return 0 without meaning EOF.547 /// This one can return 0 without meaning EOF.
537 /// TODO change to readvAdvanced548 /// TODO change to readvAdvanced
538 pub fn readAdvanced(req: *Request, buffer: []u8) !usize {549 pub fn readAdvanced(req: *Request, buffer: []u8) !usize {
539 const amt = try req.connection.read(buffer);550 var in = buffer[0..try req.connection.read(buffer)];
540 var in = buffer[0..amt];
541 var out_index: usize = 0;551 var out_index: usize = 0;
542 while (true) {552 while (true) {
543 switch (req.response.state) {553 switch (req.response.state) {
...@@ -559,7 +569,7 @@ pub const Request = struct {...@@ -559,7 +569,7 @@ pub const Request = struct {
559 if (req.redirects_left == 0) return error.TooManyHttpRedirects;569 if (req.redirects_left == 0) return error.TooManyHttpRedirects;
560 const location = req.response.headers.location orelse570 const location = req.response.headers.location orelse
561 return error.HttpRedirectMissingLocation;571 return error.HttpRedirectMissingLocation;
562 const new_url = try std.Url.parse(location);572 const new_url = try std.Uri.parse(location);
563 const new_req = try req.client.request(new_url, req.headers, .{573 const new_req = try req.client.request(new_url, req.headers, .{
564 .max_redirects = req.redirects_left - 1,574 .max_redirects = req.redirects_left - 1,
565 .header_strategy = if (req.response.header_bytes_owned) .{575 .header_strategy = if (req.response.header_bytes_owned) .{
...@@ -571,7 +581,8 @@ pub const Request = struct {...@@ -571,7 +581,8 @@ pub const Request = struct {
571 req.deinit();581 req.deinit();
572 req.* = new_req;582 req.* = new_req;
573 assert(out_index == 0);583 assert(out_index == 0);
574 return readAdvanced(req, buffer);584 in = buffer[0..try req.connection.read(buffer)];
585 continue;
575 }586 }
576587
577 if (req.response.headers.transfer_encoding) |transfer_encoding| {588 if (req.response.headers.transfer_encoding) |transfer_encoding| {
...@@ -598,8 +609,50 @@ pub const Request = struct {...@@ -598,8 +609,50 @@ pub const Request = struct {
598 return 0;609 return 0;
599 },610 },
600 .finished => {611 .finished => {
601 mem.copy(u8, buffer[out_index..], in);612 if (in.ptr == buffer.ptr) {
602 return out_index + in.len;613 return in.len;
614 } else {
615 mem.copy(u8, buffer[out_index..], in);
616 return out_index + in.len;
617 }
618 },
619 .chunk_size_prefix_r => switch (in.len) {
620 0 => return out_index,
621 1 => switch (in[0]) {
622 '\r' => {
623 req.response.state = .chunk_size_prefix_n;
624 return out_index;
625 },
626 else => {
627 req.response.state = .invalid;
628 return error.HttpHeadersInvalid;
629 },
630 },
631 else => switch (int16(in[0..2])) {
632 int16("\r\n") => {
633 in = in[2..];
634 req.response.state = .chunk_size;
635 continue;
636 },
637 else => {
638 req.response.state = .invalid;
639 return error.HttpHeadersInvalid;
640 },
641 },
642 },
643 .chunk_size_prefix_n => switch (in.len) {
644 0 => return out_index,
645 else => switch (in[0]) {
646 '\n' => {
647 in = in[1..];
648 req.response.state = .chunk_size;
649 continue;
650 },
651 else => {
652 req.response.state = .invalid;
653 return error.HttpHeadersInvalid;
654 },
655 },
603 },656 },
604 .chunk_size, .chunk_r => {657 .chunk_size, .chunk_r => {
605 const i = req.response.findChunkedLen(in);658 const i = req.response.findChunkedLen(in);
...@@ -619,20 +672,38 @@ pub const Request = struct {...@@ -619,20 +672,38 @@ pub const Request = struct {
619 },672 },
620 .chunk_data => {673 .chunk_data => {
621 const sub_amt = @min(req.response.next_chunk_length, in.len);674 const sub_amt = @min(req.response.next_chunk_length, in.len);
622 mem.copy(u8, buffer[out_index..], in[0..sub_amt]);
623 out_index += sub_amt;
624 req.response.next_chunk_length -= sub_amt;675 req.response.next_chunk_length -= sub_amt;
625 if (req.response.next_chunk_length == 0) {676 if (req.response.next_chunk_length > 0) {
626 req.response.state = .chunk_size;677 if (in.ptr == buffer.ptr) {
627 in = in[sub_amt..];678 return sub_amt;
628 continue;679 } else {
680 mem.copy(u8, buffer[out_index..], in[0..sub_amt]);
681 out_index += sub_amt;
682 return out_index;
683 }
629 }684 }
630 return out_index;685 mem.copy(u8, buffer[out_index..], in[0..sub_amt]);
686 out_index += sub_amt;
687 req.response.state = .chunk_size_prefix_r;
688 in = in[sub_amt..];
689 continue;
631 },690 },
632 }691 }
633 }692 }
634 }693 }
635694
695 inline fn int16(array: *const [2]u8) u16 {
696 return @bitCast(u16, array.*);
697 }
698
699 inline fn int32(array: *const [4]u8) u32 {
700 return @bitCast(u32, array.*);
701 }
702
703 inline fn int64(array: *const [8]u8) u64 {
704 return @bitCast(u64, array.*);
705 }
706
636 test {707 test {
637 _ = Response;708 _ = Response;
638 }709 }
...@@ -663,23 +734,25 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio...@@ -663,23 +734,25 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio
663 return conn;734 return conn;
664}735}
665736
666pub fn request(client: *Client, url: Url, headers: Request.Headers, options: Request.Options) !Request {737pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Request.Options) !Request {
667 const protocol: Connection.Protocol = if (mem.eql(u8, url.scheme, "http"))738 const protocol: Connection.Protocol = if (mem.eql(u8, uri.scheme, "http"))
668 .plain739 .plain
669 else if (mem.eql(u8, url.scheme, "https"))740 else if (mem.eql(u8, uri.scheme, "https"))
670 .tls741 .tls
671 else742 else
672 return error.UnsupportedUrlScheme;743 return error.UnsupportedUrlScheme;
673744
674 const port: u16 = url.port orelse switch (protocol) {745 const port: u16 = uri.port orelse switch (protocol) {
675 .plain => 80,746 .plain => 80,
676 .tls => 443,747 .tls => 443,
677 };748 };
678749
750 const host = uri.host orelse return error.UriMissingHost;
751
679 var req: Request = .{752 var req: Request = .{
680 .client = client,753 .client = client,
681 .headers = headers,754 .headers = headers,
682 .connection = try client.connect(url.host, port, protocol),755 .connection = try client.connect(host, port, protocol),
683 .redirects_left = options.max_redirects,756 .redirects_left = options.max_redirects,
684 .response = switch (options.header_strategy) {757 .response = switch (options.header_strategy) {
685 .dynamic => |max| Request.Response.initDynamic(max),758 .dynamic => |max| Request.Response.initDynamic(max),
...@@ -691,11 +764,11 @@ pub fn request(client: *Client, url: Url, headers: Request.Headers, options: Req...@@ -691,11 +764,11 @@ pub fn request(client: *Client, url: Url, headers: Request.Headers, options: Req
691 var h = try std.BoundedArray(u8, 1000).init(0);764 var h = try std.BoundedArray(u8, 1000).init(0);
692 try h.appendSlice(@tagName(headers.method));765 try h.appendSlice(@tagName(headers.method));
693 try h.appendSlice(" ");766 try h.appendSlice(" ");
694 try h.appendSlice(url.path);767 try h.appendSlice(uri.path);
695 try h.appendSlice(" ");768 try h.appendSlice(" ");
696 try h.appendSlice(@tagName(headers.version));769 try h.appendSlice(@tagName(headers.version));
697 try h.appendSlice("\r\nHost: ");770 try h.appendSlice("\r\nHost: ");
698 try h.appendSlice(url.host);771 try h.appendSlice(host);
699 try h.appendSlice("\r\nConnection: close\r\n\r\n");772 try h.appendSlice("\r\nConnection: close\r\n\r\n");
700773
701 const header_bytes = h.slice();774 const header_bytes = h.slice();
lib/std/std.zig+1-1
...@@ -42,7 +42,7 @@ pub const Target = @import("target.zig").Target;...@@ -42,7 +42,7 @@ pub const Target = @import("target.zig").Target;
42pub const Thread = @import("Thread.zig");42pub const Thread = @import("Thread.zig");
43pub const Treap = @import("treap.zig").Treap;43pub const Treap = @import("treap.zig").Treap;
44pub const Tz = tz.Tz;44pub const Tz = tz.Tz;
45pub const Url = @import("Url.zig");45pub const Uri = @import("Uri.zig");
4646
47pub const array_hash_map = @import("array_hash_map.zig");47pub const array_hash_map = @import("array_hash_map.zig");
48pub const atomic = @import("atomic.zig");48pub const atomic = @import("atomic.zig");