add url parsing to the std lib

This commit is contained in:
Andrew Kelley 2022-12-25 23:45:49 -07:00
parent c71c562486
commit 5b8b5f2505
4 changed files with 120 additions and 18 deletions

98
lib/std/Url.zig Normal file
View File

@ -0,0 +1,98 @@
scheme: []const u8,
host: []const u8,
path: []const u8,
port: ?u16,
/// TODO: redo this implementation according to RFC 1738. This code is only a
/// placeholder for now.
pub fn parse(s: []const u8) !Url {
var scheme_end: usize = 0;
var host_start: usize = 0;
var host_end: usize = 0;
var path_start: usize = 0;
var port_start: usize = 0;
var port_end: usize = 0;
var state: enum {
scheme,
scheme_slash1,
scheme_slash2,
host,
port,
path,
} = .scheme;
for (s) |b, i| switch (state) {
.scheme => switch (b) {
':' => {
state = .scheme_slash1;
scheme_end = i;
},
else => {},
},
.scheme_slash1 => switch (b) {
'/' => {
state = .scheme_slash2;
},
else => return error.InvalidUrl,
},
.scheme_slash2 => switch (b) {
'/' => {
state = .host;
host_start = i + 1;
},
else => return error.InvalidUrl,
},
.host => switch (b) {
':' => {
state = .port;
host_end = i;
port_start = i + 1;
},
'/' => {
state = .path;
host_end = i;
path_start = i;
},
else => {},
},
.port => switch (b) {
'/' => {
port_end = i;
state = .path;
path_start = i;
},
else => {},
},
.path => {},
};
const port_slice = s[port_start..port_end];
const port = if (port_slice.len == 0) null else try std.fmt.parseInt(u16, port_slice, 10);
return .{
.scheme = s[0..scheme_end],
.host = s[host_start..host_end],
.path = s[path_start..],
.port = port,
};
}
const Url = @This();
const std = @import("std.zig");
const testing = std.testing;
test "basic" {
const parsed = try parse("https://ziglang.org/download");
try testing.expectEqualStrings("https", parsed.scheme);
try testing.expectEqualStrings("ziglang.org", parsed.host);
try testing.expectEqualStrings("/download", parsed.path);
try testing.expectEqual(@as(?u16, null), parsed.port);
}
test "with port" {
const parsed = try parse("http://example:1337/");
try testing.expectEqualStrings("http", parsed.scheme);
try testing.expectEqualStrings("example", parsed.host);
try testing.expectEqualStrings("/", parsed.path);
try testing.expectEqual(@as(?u16, 1337), parsed.port);
}

View File

@ -105,7 +105,9 @@ pub fn addCertsFromFile(
// This is possible by computing the decoded length and reserving the space
// for the decoded bytes first.
const decoded_size_upper_bound = size / 4 * 3;
try cb.bytes.ensureUnusedCapacity(gpa, decoded_size_upper_bound + size);
const needed_capacity = std.math.cast(u32, decoded_size_upper_bound + size) orelse
return error.CertificateAuthorityBundleTooBig;
try cb.bytes.ensureUnusedCapacity(gpa, needed_capacity);
const end_reserved = cb.bytes.items.len + decoded_size_upper_bound;
const buffer = cb.bytes.allocatedSlice()[end_reserved..];
const end_index = try file.readAll(buffer);

View File

@ -3,6 +3,7 @@ const assert = std.debug.assert;
const http = std.http;
const net = std.net;
const Client = @This();
const Url = std.Url;
allocator: std.mem.Allocator,
headers: std.ArrayListUnmanaged(u8) = .{},
@ -19,14 +20,7 @@ pub const Request = struct {
pub const Protocol = enum { http, https };
pub const Options = struct {
family: Family = .any,
protocol: Protocol = .https,
method: http.Method = .GET,
host: []const u8 = "localhost",
path: []const u8 = "/",
port: u16 = 0,
pub const Family = enum { any, ip4, ip6 };
};
pub fn deinit(req: *Request) void {
@ -90,20 +84,27 @@ pub fn deinit(client: *Client) void {
client.* = undefined;
}
pub fn request(client: *Client, options: Request.Options) !Request {
pub fn request(client: *Client, url: Url, options: Request.Options) !Request {
const protocol = std.meta.stringToEnum(Request.Protocol, url.scheme) orelse
return error.UnsupportedUrlScheme;
const port: u16 = url.port orelse switch (protocol) {
.http => 80,
.https => 443,
};
var req: Request = .{
.client = client,
.stream = try net.tcpConnectToHost(client.allocator, options.host, options.port),
.protocol = options.protocol,
.stream = try net.tcpConnectToHost(client.allocator, url.host, port),
.protocol = protocol,
.tls_client = undefined,
};
client.active_requests += 1;
errdefer req.deinit();
switch (options.protocol) {
switch (protocol) {
.http => {},
.https => {
req.tls_client = try std.crypto.tls.Client.init(req.stream, client.ca_bundle, options.host);
req.tls_client = try std.crypto.tls.Client.init(req.stream, client.ca_bundle, url.host);
},
}
@ -111,19 +112,19 @@ pub fn request(client: *Client, options: Request.Options) !Request {
client.allocator,
@tagName(options.method).len +
1 +
options.path.len +
url.path.len +
" HTTP/1.1\r\nHost: ".len +
options.host.len +
url.host.len +
"\r\nUpgrade-Insecure-Requests: 1\r\n".len +
client.headers.items.len +
2, // for the \r\n at the end of headers
);
req.headers.appendSliceAssumeCapacity(@tagName(options.method));
req.headers.appendSliceAssumeCapacity(" ");
req.headers.appendSliceAssumeCapacity(options.path);
req.headers.appendSliceAssumeCapacity(url.path);
req.headers.appendSliceAssumeCapacity(" HTTP/1.1\r\nHost: ");
req.headers.appendSliceAssumeCapacity(options.host);
switch (options.protocol) {
req.headers.appendSliceAssumeCapacity(url.host);
switch (protocol) {
.https => req.headers.appendSliceAssumeCapacity("\r\nUpgrade-Insecure-Requests: 1\r\n"),
.http => req.headers.appendSliceAssumeCapacity("\r\n"),
}

View File

@ -42,6 +42,7 @@ pub const Target = @import("target.zig").Target;
pub const Thread = @import("Thread.zig");
pub const Treap = @import("treap.zig").Treap;
pub const Tz = tz.Tz;
pub const Url = @import("Url.zig");
pub const array_hash_map = @import("array_hash_map.zig");
pub const atomic = @import("atomic.zig");