Commit 5b8b5f2505

Andrew Kelley <andrew@ziglang.org>
2022-12-26 07:45:49
add url parsing to the std lib
1 parent c71c562
Changed files (4)
lib
std
crypto
Certificate
http
lib/std/crypto/Certificate/Bundle.zig
@@ -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);
lib/std/http/Client.zig
@@ -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"),
     }
lib/std/std.zig
@@ -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");
lib/std/Url.zig
@@ -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);
+}