Commit b428612a20
Changed files (10)
lib
lib/std/fs/test.zig
@@ -2281,7 +2281,7 @@ test "seekTo flushes buffered data" {
}
var read_buffer: [16]u8 = undefined;
- var file_reader: std.Io.File.Reader = .init(file, io, &read_buffer);
+ var file_reader: std.Io.File.Reader = .initAdapted(file, io, &read_buffer);
var buf: [4]u8 = undefined;
try file_reader.interface.readSliceAll(&buf);
lib/std/http/Client.zig
@@ -15,6 +15,7 @@ const assert = std.debug.assert;
const Io = std.Io;
const Writer = std.Io.Writer;
const Reader = std.Io.Reader;
+const HostName = std.Io.net.HostName;
const Client = @This();
@@ -69,7 +70,7 @@ pub const ConnectionPool = struct {
/// The criteria for a connection to be considered a match.
pub const Criteria = struct {
- host: []const u8,
+ host: HostName,
port: u16,
protocol: Protocol,
};
@@ -89,7 +90,7 @@ pub const ConnectionPool = struct {
if (connection.port != criteria.port) continue;
// Domain names are case-insensitive (RFC 5890, Section 2.3.2.4)
- if (!std.ascii.eqlIgnoreCase(connection.host(), criteria.host)) continue;
+ if (!connection.host().eql(criteria.host)) continue;
pool.acquireUnsafe(connection);
return connection;
@@ -118,19 +119,19 @@ pub const ConnectionPool = struct {
/// If the connection is marked as closing, it will be closed instead.
///
/// Threadsafe.
- pub fn release(pool: *ConnectionPool, connection: *Connection) void {
+ pub fn release(pool: *ConnectionPool, connection: *Connection, io: Io) void {
pool.mutex.lock();
defer pool.mutex.unlock();
pool.used.remove(&connection.pool_node);
- if (connection.closing or pool.free_size == 0) return connection.destroy();
+ if (connection.closing or pool.free_size == 0) return connection.destroy(io);
if (pool.free_len >= pool.free_size) {
const popped: *Connection = @alignCast(@fieldParentPtr("pool_node", pool.free.popFirst().?));
pool.free_len -= 1;
- popped.destroy();
+ popped.destroy(io);
}
if (connection.proxied) {
@@ -178,21 +179,21 @@ pub const ConnectionPool = struct {
/// All future operations on the connection pool will deadlock.
///
/// Threadsafe.
- pub fn deinit(pool: *ConnectionPool) void {
+ pub fn deinit(pool: *ConnectionPool, io: Io) void {
pool.mutex.lock();
var next = pool.free.first;
while (next) |node| {
const connection: *Connection = @alignCast(@fieldParentPtr("pool_node", node));
next = node.next;
- connection.destroy();
+ connection.destroy(io);
}
next = pool.used.first;
while (next) |node| {
const connection: *Connection = @alignCast(@fieldParentPtr("pool_node", node));
next = node.next;
- connection.destroy();
+ connection.destroy(io);
}
pool.* = undefined;
@@ -242,19 +243,19 @@ pub const Connection = struct {
fn create(
client: *Client,
- remote_host: []const u8,
+ remote_host: HostName,
port: u16,
stream: Io.net.Stream,
) error{OutOfMemory}!*Plain {
const gpa = client.allocator;
- const alloc_len = allocLen(client, remote_host.len);
+ const alloc_len = allocLen(client, remote_host.bytes.len);
const base = try gpa.alignedAlloc(u8, .of(Plain), alloc_len);
errdefer gpa.free(base);
- const host_buffer = base[@sizeOf(Plain)..][0..remote_host.len];
+ const host_buffer = base[@sizeOf(Plain)..][0..remote_host.bytes.len];
const socket_read_buffer = host_buffer.ptr[host_buffer.len..][0..client.read_buffer_size];
const socket_write_buffer = socket_read_buffer.ptr[socket_read_buffer.len..][0..client.write_buffer_size];
assert(base.ptr + alloc_len == socket_write_buffer.ptr + socket_write_buffer.len);
- @memcpy(host_buffer, remote_host);
+ @memcpy(host_buffer, remote_host.bytes);
const plain: *Plain = @ptrCast(base);
plain.* = .{
.connection = .{
@@ -263,7 +264,7 @@ pub const Connection = struct {
.stream_reader = stream.reader(socket_read_buffer),
.pool_node = .{},
.port = port,
- .host_len = @intCast(remote_host.len),
+ .host_len = @intCast(remote_host.bytes.len),
.proxied = false,
.closing = false,
.protocol = .plain,
@@ -283,9 +284,9 @@ pub const Connection = struct {
return @sizeOf(Plain) + host_len + client.read_buffer_size + client.write_buffer_size;
}
- fn host(plain: *Plain) []u8 {
+ fn host(plain: *Plain) HostName {
const base: [*]u8 = @ptrCast(plain);
- return base[@sizeOf(Plain)..][0..plain.connection.host_len];
+ return .{ .bytes = base[@sizeOf(Plain)..][0..plain.connection.host_len] };
}
};
@@ -295,15 +296,15 @@ pub const Connection = struct {
fn create(
client: *Client,
- remote_host: []const u8,
+ remote_host: HostName,
port: u16,
stream: Io.net.Stream,
) error{ OutOfMemory, TlsInitializationFailed }!*Tls {
const gpa = client.allocator;
- const alloc_len = allocLen(client, remote_host.len);
+ const alloc_len = allocLen(client, remote_host.bytes.len);
const base = try gpa.alignedAlloc(u8, .of(Tls), alloc_len);
errdefer gpa.free(base);
- const host_buffer = base[@sizeOf(Tls)..][0..remote_host.len];
+ const host_buffer = base[@sizeOf(Tls)..][0..remote_host.bytes.len];
// The TLS client wants enough buffer for the max encrypted frame
// size, and the HTTP body reader wants enough buffer for the
// entire HTTP header. This means we need a combined upper bound.
@@ -313,7 +314,7 @@ pub const Connection = struct {
const socket_write_buffer = tls_write_buffer.ptr[tls_write_buffer.len..][0..client.write_buffer_size];
const socket_read_buffer = socket_write_buffer.ptr[socket_write_buffer.len..][0..client.tls_buffer_size];
assert(base.ptr + alloc_len == socket_read_buffer.ptr + socket_read_buffer.len);
- @memcpy(host_buffer, remote_host);
+ @memcpy(host_buffer, remote_host.bytes);
const tls: *Tls = @ptrCast(base);
tls.* = .{
.connection = .{
@@ -322,17 +323,17 @@ pub const Connection = struct {
.stream_reader = stream.reader(socket_read_buffer),
.pool_node = .{},
.port = port,
- .host_len = @intCast(remote_host.len),
+ .host_len = @intCast(remote_host.bytes.len),
.proxied = false,
.closing = false,
.protocol = .tls,
},
// TODO data race here on ca_bundle if the user sets next_https_rescan_certs to true
.client = std.crypto.tls.Client.init(
- tls.connection.stream_reader.interface(),
+ &tls.connection.stream_reader.interface,
&tls.connection.stream_writer.interface,
.{
- .host = .{ .explicit = remote_host },
+ .host = .{ .explicit = remote_host.bytes },
.ca = .{ .bundle = client.ca_bundle },
.ssl_key_log = client.ssl_key_log,
.read_buffer = tls_read_buffer,
@@ -359,9 +360,9 @@ pub const Connection = struct {
client.write_buffer_size + client.tls_buffer_size;
}
- fn host(tls: *Tls) []u8 {
+ fn host(tls: *Tls) HostName {
const base: [*]u8 = @ptrCast(tls);
- return base[@sizeOf(Tls)..][0..tls.connection.host_len];
+ return .{ .bytes = base[@sizeOf(Tls)..][0..tls.connection.host_len] };
}
};
@@ -384,7 +385,7 @@ pub const Connection = struct {
return c.stream_reader.stream;
}
- pub fn host(c: *Connection) []u8 {
+ pub fn host(c: *Connection) HostName {
return switch (c.protocol) {
.tls => {
if (disable_tls) unreachable;
@@ -400,8 +401,8 @@ pub const Connection = struct {
/// If this is called without calling `flush` or `end`, data will be
/// dropped unsent.
- pub fn destroy(c: *Connection) void {
- c.getStream().close();
+ pub fn destroy(c: *Connection, io: Io) void {
+ c.stream_reader.stream.close(io);
switch (c.protocol) {
.tls => {
if (disable_tls) unreachable;
@@ -437,7 +438,7 @@ pub const Connection = struct {
const tls: *Tls = @alignCast(@fieldParentPtr("connection", c));
return &tls.client.reader;
},
- .plain => c.stream_reader.interface(),
+ .plain => &c.stream_reader.interface,
};
}
@@ -866,6 +867,7 @@ pub const Request = struct {
/// Returns the request's `Connection` back to the pool of the `Client`.
pub fn deinit(r: *Request) void {
+ const io = r.client.io;
if (r.connection) |connection| {
connection.closing = connection.closing or switch (r.reader.state) {
.ready => false,
@@ -880,7 +882,7 @@ pub const Request = struct {
},
else => true,
};
- r.client.connection_pool.release(connection);
+ r.client.connection_pool.release(connection, io);
}
r.* = undefined;
}
@@ -1182,6 +1184,7 @@ pub const Request = struct {
///
/// `aux_buf` must outlive accesses to `Request.uri`.
fn redirect(r: *Request, head: *const Response.Head, aux_buf: *[]u8) !void {
+ const io = r.client.io;
const new_location = head.location orelse return error.HttpRedirectLocationMissing;
if (new_location.len > aux_buf.*.len) return error.HttpRedirectLocationOversize;
const location = aux_buf.*[0..new_location.len];
@@ -1204,13 +1207,13 @@ pub const Request = struct {
const protocol = Protocol.fromUri(new_uri) orelse return error.UnsupportedUriScheme;
const old_connection = r.connection.?;
const old_host = old_connection.host();
- var new_host_name_buffer: [Uri.host_name_max]u8 = undefined;
+ var new_host_name_buffer: [HostName.max_len]u8 = undefined;
const new_host = try new_uri.getHost(&new_host_name_buffer);
const keep_privileged_headers =
std.ascii.eqlIgnoreCase(r.uri.scheme, new_uri.scheme) and
- sameParentDomain(old_host, new_host);
+ old_host.sameParentDomain(new_host);
- r.client.connection_pool.release(old_connection);
+ r.client.connection_pool.release(old_connection, io);
r.connection = null;
if (!keep_privileged_headers) {
@@ -1266,7 +1269,7 @@ pub const Request = struct {
pub const Proxy = struct {
protocol: Protocol,
- host: []const u8,
+ host: HostName,
authorization: ?[]const u8,
port: u16,
supports_connect: bool,
@@ -1277,9 +1280,10 @@ pub const Proxy = struct {
/// All pending requests must be de-initialized and all active connections released
/// before calling this function.
pub fn deinit(client: *Client) void {
+ const io = client.io;
assert(client.connection_pool.used.first == null); // There are still active requests.
- client.connection_pool.deinit();
+ client.connection_pool.deinit(io);
if (!disable_tls) client.ca_bundle.deinit(client.allocator);
client.* = undefined;
@@ -1385,7 +1389,7 @@ pub const basic_authorization = struct {
}
};
-pub const ConnectTcpError = Allocator.Error || error{
+pub const ConnectTcpError = error{
ConnectionRefused,
NetworkUnreachable,
ConnectionTimedOut,
@@ -1393,17 +1397,16 @@ pub const ConnectTcpError = Allocator.Error || error{
TemporaryNameServerFailure,
NameServerFailure,
UnknownHostName,
- HostLacksNetworkAddresses,
UnexpectedConnectFailure,
TlsInitializationFailed,
-};
+} || Allocator.Error || Io.Cancelable;
/// Reuses a `Connection` if one matching `host` and `port` is already open.
///
/// Threadsafe.
pub fn connectTcp(
client: *Client,
- host: []const u8,
+ host: HostName,
port: u16,
protocol: Protocol,
) ConnectTcpError!*Connection {
@@ -1411,16 +1414,17 @@ pub fn connectTcp(
}
pub const ConnectTcpOptions = struct {
- host: Io.net.HostName,
+ host: HostName,
port: u16,
protocol: Protocol,
- proxied_host: ?[]const u8 = null,
+ proxied_host: ?HostName = null,
proxied_port: ?u16 = null,
+ timeout: Io.Timeout = .none,
};
pub fn connectTcpOptions(client: *Client, options: ConnectTcpOptions) ConnectTcpError!*Connection {
- const host = options.host_name;
+ const host = options.host;
const port = options.port;
const protocol = options.protocol;
@@ -1433,17 +1437,15 @@ pub fn connectTcpOptions(client: *Client, options: ConnectTcpOptions) ConnectTcp
.protocol = protocol,
})) |conn| return conn;
- const stream = host.connectTcp(client.io, port) catch |err| switch (err) {
+ const stream = host.connect(client.io, port, .{ .mode = .stream }) catch |err| switch (err) {
error.ConnectionRefused => return error.ConnectionRefused,
error.NetworkUnreachable => return error.NetworkUnreachable,
error.ConnectionTimedOut => return error.ConnectionTimedOut,
error.ConnectionResetByPeer => return error.ConnectionResetByPeer,
- error.TemporaryNameServerFailure => return error.TemporaryNameServerFailure,
error.NameServerFailure => return error.NameServerFailure,
error.UnknownHostName => return error.UnknownHostName,
- error.HostLacksNetworkAddresses => return error.HostLacksNetworkAddresses,
error.Canceled => return error.Canceled,
- else => return error.UnexpectedConnectFailure,
+ //else => return error.UnexpectedConnectFailure,
};
errdefer stream.close();
@@ -1479,7 +1481,7 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti
errdefer client.allocator.destroy(conn);
conn.* = .{ .data = undefined };
- const stream = try std.net.connectUnixSocket(path);
+ const stream = try Io.net.connectUnixSocket(path);
errdefer stream.close();
conn.data = .{
@@ -1504,9 +1506,10 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti
pub fn connectProxied(
client: *Client,
proxy: *Proxy,
- proxied_host: []const u8,
+ proxied_host: HostName,
proxied_port: u16,
) !*Connection {
+ const io = client.io;
if (!proxy.supports_connect) return error.TunnelNotSupported;
if (client.connection_pool.findConnection(.{
@@ -1526,12 +1529,12 @@ pub fn connectProxied(
});
errdefer {
connection.closing = true;
- client.connection_pool.release(connection);
+ client.connection_pool.release(connection, io);
}
var req = client.request(.CONNECT, .{
.scheme = "http",
- .host = .{ .raw = proxied_host },
+ .host = .{ .raw = proxied_host.bytes },
.port = proxied_port,
}, .{
.redirect_behavior = .unhandled,
@@ -1576,7 +1579,7 @@ pub const ConnectError = ConnectTcpError || RequestError;
/// This function is threadsafe.
pub fn connect(
client: *Client,
- host: []const u8,
+ host: HostName,
port: u16,
protocol: Protocol,
) ConnectError!*Connection {
@@ -1586,9 +1589,7 @@ pub fn connect(
} orelse return client.connectTcp(host, port, protocol);
// Prevent proxying through itself.
- if (std.ascii.eqlIgnoreCase(proxy.host, host) and
- proxy.port == port and proxy.protocol == protocol)
- {
+ if (proxy.host.eql(host) and proxy.port == port and proxy.protocol == protocol) {
return client.connectTcp(host, port, protocol);
}
@@ -1608,7 +1609,6 @@ pub fn connect(
pub const RequestError = ConnectTcpError || error{
UnsupportedUriScheme,
UriMissingHost,
- UriHostTooLong,
CertificateBundleLoadFailure,
};
@@ -1697,7 +1697,7 @@ pub fn request(
}
const connection = options.connection orelse c: {
- var host_name_buffer: [Uri.host_name_max]u8 = undefined;
+ var host_name_buffer: [HostName.max_len]u8 = undefined;
const host_name = try uri.getHost(&host_name_buffer);
break :c try client.connect(host_name, uriPort(uri, protocol), protocol);
};
@@ -1835,20 +1835,6 @@ pub fn fetch(client: *Client, options: FetchOptions) FetchError!FetchResult {
return .{ .status = response.head.status };
}
-pub fn sameParentDomain(parent_host: []const u8, child_host: []const u8) bool {
- if (!std.ascii.endsWithIgnoreCase(child_host, parent_host)) return false;
- if (child_host.len == parent_host.len) return true;
- if (parent_host.len > child_host.len) return false;
- return child_host[child_host.len - parent_host.len - 1] == '.';
-}
-
-test sameParentDomain {
- try testing.expect(!sameParentDomain("foo.com", "bar.com"));
- try testing.expect(sameParentDomain("foo.com", "foo.com"));
- try testing.expect(sameParentDomain("foo.com", "bar.foo.com"));
- try testing.expect(!sameParentDomain("bar.foo.com", "foo.com"));
-}
-
test {
_ = Response;
}
lib/std/http/test.zig
@@ -53,7 +53,7 @@ test "trailers" {
const gpa = std.testing.allocator;
- var client: http.Client = .{ .allocator = gpa };
+ var client: http.Client = .{ .allocator = gpa, .io = io };
defer client.deinit();
const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/trailer", .{
@@ -141,12 +141,13 @@ test "HTTP server handles a chunked transfer coding request" {
"0\r\n" ++
"\r\n";
- const gpa = std.testing.allocator;
- var stream = try net.tcpConnectToHost(gpa, "127.0.0.1", test_server.port());
+ const host_name: net.HostName = try .init("127.0.0.1");
+ var stream = try host_name.connect(io, test_server.port(), .{ .mode = .stream });
defer stream.close(io);
- var stream_writer = stream.writer(&.{});
+ var stream_writer = stream.writer(io, &.{});
try stream_writer.interface.writeAll(request_bytes);
+ const gpa = std.testing.allocator;
const expected_response =
"HTTP/1.1 200 OK\r\n" ++
"connection: close\r\n" ++
@@ -154,8 +155,8 @@ test "HTTP server handles a chunked transfer coding request" {
"content-type: text/plain\r\n" ++
"\r\n" ++
"message from server!\n";
- var stream_reader = stream.reader(&.{});
- const response = try stream_reader.interface().allocRemaining(gpa, .limited(expected_response.len + 1));
+ var stream_reader = stream.reader(io, &.{});
+ const response = try stream_reader.interface.allocRemaining(gpa, .limited(expected_response.len + 1));
defer gpa.free(response);
try expectEqualStrings(expected_response, response);
}
@@ -241,7 +242,7 @@ test "echo content server" {
defer test_server.destroy();
{
- var client: http.Client = .{ .allocator = std.testing.allocator };
+ var client: http.Client = .{ .allocator = std.testing.allocator, .io = io };
defer client.deinit();
try echoTests(&client, test_server.port());
@@ -294,14 +295,15 @@ test "Server.Request.respondStreaming non-chunked, unknown content-length" {
defer test_server.destroy();
const request_bytes = "GET /foo HTTP/1.1\r\n\r\n";
- const gpa = std.testing.allocator;
- var stream = try net.tcpConnectToHost(gpa, "127.0.0.1", test_server.port());
+ const host_name: net.HostName = try .init("127.0.0.1");
+ var stream = try host_name.connect(io, test_server.port(), .{ .mode = .stream });
defer stream.close(io);
- var stream_writer = stream.writer(&.{});
+ var stream_writer = stream.writer(io, &.{});
try stream_writer.interface.writeAll(request_bytes);
- var stream_reader = stream.reader(&.{});
- const response = try stream_reader.interface().allocRemaining(gpa, .unlimited);
+ var stream_reader = stream.reader(io, &.{});
+ const gpa = std.testing.allocator;
+ const response = try stream_reader.interface.allocRemaining(gpa, .unlimited);
defer gpa.free(response);
var expected_response = std.array_list.Managed(u8).init(gpa);
@@ -366,14 +368,15 @@ test "receiving arbitrary http headers from the client" {
"CoNneCtIoN:close\r\n" ++
"aoeu: asdf \r\n" ++
"\r\n";
- const gpa = std.testing.allocator;
- var stream = try net.tcpConnectToHost(gpa, "127.0.0.1", test_server.port());
+ const host_name: net.HostName = try .init("127.0.0.1");
+ var stream = try host_name.connect(io, test_server.port(), .{ .mode = .stream });
defer stream.close(io);
- var stream_writer = stream.writer(&.{});
+ var stream_writer = stream.writer(io, &.{});
try stream_writer.interface.writeAll(request_bytes);
- var stream_reader = stream.reader(&.{});
- const response = try stream_reader.interface().allocRemaining(gpa, .unlimited);
+ var stream_reader = stream.reader(io, &.{});
+ const gpa = std.testing.allocator;
+ const response = try stream_reader.interface.allocRemaining(gpa, .unlimited);
defer gpa.free(response);
var expected_response = std.array_list.Managed(u8).init(gpa);
@@ -413,7 +416,7 @@ test "general client/server API coverage" {
else => |e| return e,
};
- try handleRequest(&request, net_server.listen_address.getPort());
+ try handleRequest(&request, net_server.socket.address.getPort());
}
}
}
@@ -543,9 +546,9 @@ test "general client/server API coverage" {
fn getUnusedTcpPort() !u16 {
const addr = try net.IpAddress.parse("127.0.0.1", 0);
- var s = try addr.listen(.{});
- defer s.deinit();
- return s.listen_address.in.getPort();
+ var s = try addr.listen(io, .{});
+ defer s.deinit(io);
+ return s.socket.address.getPort();
}
});
defer test_server.destroy();
@@ -553,7 +556,7 @@ test "general client/server API coverage" {
const log = std.log.scoped(.client);
const gpa = std.testing.allocator;
- var client: http.Client = .{ .allocator = gpa };
+ var client: http.Client = .{ .allocator = gpa, .io = io };
defer client.deinit();
const port = test_server.port();
@@ -918,7 +921,10 @@ test "Server streams both reading and writing" {
});
defer test_server.destroy();
- var client: http.Client = .{ .allocator = std.testing.allocator };
+ var client: http.Client = .{
+ .allocator = std.testing.allocator,
+ .io = io,
+ };
defer client.deinit();
var redirect_buffer: [555]u8 = undefined;
@@ -1089,17 +1095,20 @@ fn echoTests(client: *http.Client, port: u16) !void {
}
const TestServer = struct {
+ io: Io,
shutting_down: bool,
server_thread: std.Thread,
net_server: net.Server,
fn destroy(self: *@This()) void {
+ const io = self.io;
self.shutting_down = true;
- const conn = net.tcpConnectToAddress(self.net_server.listen_address) catch @panic("shutdown failure");
- conn.close();
+ var stream = self.net_server.socket.address.connect(io, .{ .mode = .stream }) catch
+ @panic("shutdown failure");
+ stream.close(io);
self.server_thread.join();
- self.net_server.deinit();
+ self.net_server.deinit(io);
std.testing.allocator.destroy(self);
}
@@ -1118,6 +1127,7 @@ fn createTestServer(io: Io, S: type) !*TestServer {
const address = try net.IpAddress.parse("127.0.0.1", 0);
const test_server = try std.testing.allocator.create(TestServer);
test_server.* = .{
+ .io = io,
.net_server = try address.listen(io, .{ .reuse_address = true }),
.shutting_down = false,
.server_thread = try std.Thread.spawn(.{}, S.run, .{test_server}),
lib/std/Io/net/HostName.zig
@@ -19,12 +19,12 @@ bytes: []const u8,
pub const max_len = 255;
-pub const InitError = error{
+pub const ValidateError = error{
NameTooLong,
InvalidHostName,
};
-pub fn init(bytes: []const u8) InitError!HostName {
+pub fn validate(bytes: []const u8) ValidateError!void {
if (bytes.len > max_len) return error.NameTooLong;
if (!std.unicode.utf8ValidateSlice(bytes)) return error.InvalidHostName;
for (bytes) |byte| {
@@ -33,10 +33,34 @@ pub fn init(bytes: []const u8) InitError!HostName {
}
return error.InvalidHostName;
}
+}
+
+pub fn init(bytes: []const u8) ValidateError!HostName {
+ try validate(bytes);
return .{ .bytes = bytes };
}
-/// TODO add a retry field here
+pub fn sameParentDomain(parent_host: HostName, child_host: HostName) bool {
+ const parent_bytes = parent_host.bytes;
+ const child_bytes = child_host.bytes;
+ if (!std.ascii.endsWithIgnoreCase(child_bytes, parent_bytes)) return false;
+ if (child_bytes.len == parent_bytes.len) return true;
+ if (parent_bytes.len > child_bytes.len) return false;
+ return child_bytes[child_bytes.len - parent_bytes.len - 1] == '.';
+}
+
+test sameParentDomain {
+ try std.testing.expect(!sameParentDomain(try .init("foo.com"), try .init("bar.com")));
+ try std.testing.expect(sameParentDomain(try .init("foo.com"), try .init("foo.com")));
+ try std.testing.expect(sameParentDomain(try .init("foo.com"), try .init("bar.foo.com")));
+ try std.testing.expect(!sameParentDomain(try .init("bar.foo.com"), try .init("foo.com")));
+}
+
+/// Domain names are case-insensitive (RFC 5890, Section 2.3.2.4)
+pub fn eql(a: HostName, b: HostName) bool {
+ return std.ascii.eqlIgnoreCase(a.bytes, b.bytes);
+}
+
pub const LookupOptions = struct {
port: u16,
/// Must have at least length 2.
@@ -266,15 +290,15 @@ fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, optio
var answers_remaining = answers.len;
for (answers) |*answer| answer.len = 0;
- // boottime is chosen because time the computer is suspended should count
+ // boot clock is chosen because time the computer is suspended should count
// against time spent waiting for external messages to arrive.
- var now_ts = try Io.Timestamp.now(io, .boottime);
+ var now_ts = try Io.Timestamp.now(io, .boot);
const final_ts = now_ts.addDuration(.fromSeconds(rc.timeout_seconds));
const attempt_duration: Io.Duration = .{
.nanoseconds = std.time.ns_per_s * @as(usize, rc.timeout_seconds) / rc.attempts,
};
- send: while (now_ts.compare(.lt, final_ts)) : (now_ts = try Io.Timestamp.now(io, .boottime)) {
+ send: while (now_ts.compare(.lt, final_ts)) : (now_ts = try Io.Timestamp.now(io, .boot)) {
const max_messages = queries_buffer.len * ResolvConf.max_nameservers;
{
var message_buffer: [max_messages]Io.net.OutgoingMessage = undefined;
@@ -518,7 +542,7 @@ fn writeResolutionQuery(q: *[280]u8, op: u4, dname: []const u8, class: u8, ty: u
return n;
}
-pub const ExpandError = error{InvalidDnsPacket} || InitError;
+pub const ExpandError = error{InvalidDnsPacket} || ValidateError;
/// Decompresses a DNS name.
///
@@ -618,22 +642,36 @@ pub const DnsResponse = struct {
}
};
-pub const ConnectTcpError = LookupError || IpAddress.ConnectTcpError;
+pub const ConnectError = LookupError || IpAddress.ConnectError;
-pub fn connectTcp(host_name: HostName, io: Io, port: u16) ConnectTcpError!Stream {
+pub fn connect(
+ host_name: HostName,
+ io: Io,
+ port: u16,
+ options: IpAddress.ConnectOptions,
+) ConnectError!Stream {
var addresses_buffer: [32]IpAddress = undefined;
+ var canonical_name_buffer: [HostName.max_len]u8 = undefined;
- const results = try lookup(host_name, .{
+ const results = try lookup(host_name, io, .{
.port = port,
.addresses_buffer = &addresses_buffer,
- .canonical_name_buffer = &.{},
+ .canonical_name_buffer = &canonical_name_buffer,
});
const addresses = addresses_buffer[0..results.addresses_len];
if (addresses.len == 0) return error.UnknownHostName;
- for (addresses) |addr| {
- return addr.connectTcp(io) catch |err| switch (err) {
+ // TODO instead of serially, use a Select API to send out
+ // the connections simultaneously and then keep the first
+ // successful one, canceling the rest.
+
+ // TODO On Linux this should additionally use an Io.Queue based
+ // DNS resolution API in order to send out a connection after
+ // each DNS response before waiting for the rest of them.
+
+ for (addresses) |*addr| {
+ return addr.connect(io, options) catch |err| switch (err) {
error.ConnectionRefused => continue,
else => |e| return e,
};
lib/std/Io/net/test.zig
@@ -7,32 +7,30 @@ const testing = std.testing;
test "parse and render IP addresses at comptime" {
comptime {
const ipv6addr = net.IpAddress.parse("::1", 0) catch unreachable;
- try std.testing.expectFmt("[::1]:0", "{f}", .{ipv6addr});
+ try testing.expectFmt("[::1]:0", "{f}", .{ipv6addr});
const ipv4addr = net.IpAddress.parse("127.0.0.1", 0) catch unreachable;
- try std.testing.expectFmt("127.0.0.1:0", "{f}", .{ipv4addr});
+ try testing.expectFmt("127.0.0.1:0", "{f}", .{ipv4addr});
try testing.expectError(error.ParseFailed, net.IpAddress.parse("::123.123.123.123", 0));
try testing.expectError(error.ParseFailed, net.IpAddress.parse("127.01.0.1", 0));
- try testing.expectError(error.ParseFailed, net.IpAddress.resolveIp("::123.123.123.123", 0));
- try testing.expectError(error.ParseFailed, net.IpAddress.resolveIp("127.01.0.1", 0));
}
}
test "format IPv6 address with no zero runs" {
- const addr = try std.net.IpAddress.parseIp6("2001:db8:1:2:3:4:5:6", 0);
- try std.testing.expectFmt("[2001:db8:1:2:3:4:5:6]:0", "{f}", .{addr});
+ const addr = try net.IpAddress.parseIp6("2001:db8:1:2:3:4:5:6", 0);
+ try testing.expectFmt("[2001:db8:1:2:3:4:5:6]:0", "{f}", .{addr});
}
test "parse IPv6 addresses and check compressed form" {
- try std.testing.expectFmt("[2001:db8::1:0:0:2]:0", "{f}", .{
- try std.net.IpAddress.parseIp6("2001:0db8:0000:0000:0001:0000:0000:0002", 0),
+ try testing.expectFmt("[2001:db8::1:0:0:2]:0", "{f}", .{
+ try net.IpAddress.parseIp6("2001:0db8:0000:0000:0001:0000:0000:0002", 0),
});
- try std.testing.expectFmt("[2001:db8::1:2]:0", "{f}", .{
- try std.net.IpAddress.parseIp6("2001:0db8:0000:0000:0000:0000:0001:0002", 0),
+ try testing.expectFmt("[2001:db8::1:2]:0", "{f}", .{
+ try net.IpAddress.parseIp6("2001:0db8:0000:0000:0000:0000:0001:0002", 0),
});
- try std.testing.expectFmt("[2001:db8:1:0:1::2]:0", "{f}", .{
- try std.net.IpAddress.parseIp6("2001:0db8:0001:0000:0001:0000:0000:0002", 0),
+ try testing.expectFmt("[2001:db8:1:0:1::2]:0", "{f}", .{
+ try net.IpAddress.parseIp6("2001:0db8:0001:0000:0001:0000:0000:0002", 0),
});
}
@@ -43,14 +41,14 @@ test "parse IPv6 address, check raw bytes" {
0x00, 0x01, 0x00, 0x00, // :0001:0000
0x00, 0x00, 0x00, 0x02, // :0000:0002
};
-
- const addr = try std.net.IpAddress.parseIp6("2001:db8:0000:0000:0001:0000:0000:0002", 0);
-
- const actual_raw = addr.in6.sa.addr[0..];
- try std.testing.expectEqualSlices(u8, expected_raw[0..], actual_raw);
+ const addr = try net.IpAddress.parseIp6("2001:db8:0000:0000:0001:0000:0000:0002", 0);
+ try testing.expectEqualSlices(u8, &expected_raw, &addr.ip6.bytes);
}
test "parse and render IPv6 addresses" {
+ // TODO make this test parsing and rendering only, then it doesn't need I/O
+ const io = testing.io;
+
var buffer: [100]u8 = undefined;
const ips = [_][]const u8{
"FF01:0:0:0:0:0:0:FB",
@@ -79,12 +77,12 @@ test "parse and render IPv6 addresses" {
for (ips, 0..) |ip, i| {
const addr = net.IpAddress.parseIp6(ip, 0) catch unreachable;
var newIp = std.fmt.bufPrint(buffer[0..], "{f}", .{addr}) catch unreachable;
- try std.testing.expect(std.mem.eql(u8, printed[i], newIp[1 .. newIp.len - 3]));
+ try testing.expect(std.mem.eql(u8, printed[i], newIp[1 .. newIp.len - 3]));
if (builtin.os.tag == .linux) {
- const addr_via_resolve = net.IpAddress.resolveIp6(ip, 0) catch unreachable;
+ const addr_via_resolve = net.IpAddress.resolveIp6(io, ip, 0) catch unreachable;
var newResolvedIp = std.fmt.bufPrint(buffer[0..], "{f}", .{addr_via_resolve}) catch unreachable;
- try std.testing.expect(std.mem.eql(u8, printed[i], newResolvedIp[1 .. newResolvedIp.len - 3]));
+ try testing.expect(std.mem.eql(u8, printed[i], newResolvedIp[1 .. newResolvedIp.len - 3]));
}
}
@@ -97,21 +95,23 @@ test "parse and render IPv6 addresses" {
try testing.expectError(error.Incomplete, net.IpAddress.parseIp6("1", 0));
// TODO Make this test pass on other operating systems.
if (builtin.os.tag == .linux or comptime builtin.os.tag.isDarwin() or builtin.os.tag == .windows) {
- try testing.expectError(error.Incomplete, net.IpAddress.resolveIp6("ff01::fb%", 0));
+ try testing.expectError(error.Incomplete, net.IpAddress.resolveIp6(io, "ff01::fb%", 0));
// Assumes IFNAMESIZE will always be a multiple of 2
- try testing.expectError(error.Overflow, net.IpAddress.resolveIp6("ff01::fb%wlp3" ++ "s0" ** @divExact(std.posix.IFNAMESIZE - 4, 2), 0));
- try testing.expectError(error.Overflow, net.IpAddress.resolveIp6("ff01::fb%12345678901234", 0));
+ try testing.expectError(error.Overflow, net.IpAddress.resolveIp6(io, "ff01::fb%wlp3" ++ "s0" ** @divExact(std.posix.IFNAMESIZE - 4, 2), 0));
+ try testing.expectError(error.Overflow, net.IpAddress.resolveIp6(io, "ff01::fb%12345678901234", 0));
}
}
test "invalid but parseable IPv6 scope ids" {
+ const io = testing.io;
+
if (builtin.os.tag != .linux and comptime !builtin.os.tag.isDarwin() and builtin.os.tag != .windows) {
// Currently, resolveIp6 with alphanumerical scope IDs only works on Linux.
// TODO Make this test pass on other operating systems.
return error.SkipZigTest;
}
- try testing.expectError(error.InterfaceNotFound, net.IpAddress.resolveIp6("ff01::fb%123s45678901234", 0));
+ try testing.expectError(error.InterfaceNotFound, net.IpAddress.resolveIp6(io, "ff01::fb%123s45678901234", 0));
}
test "parse and render IPv4 addresses" {
@@ -125,7 +125,7 @@ test "parse and render IPv4 addresses" {
}) |ip| {
const addr = net.IpAddress.parseIp4(ip, 0) catch unreachable;
var newIp = std.fmt.bufPrint(buffer[0..], "{f}", .{addr}) catch unreachable;
- try std.testing.expect(std.mem.eql(u8, ip, newIp[0 .. newIp.len - 2]));
+ try testing.expect(std.mem.eql(u8, ip, newIp[0 .. newIp.len - 2]));
}
try testing.expectError(error.Overflow, net.IpAddress.parseIp4("256.0.0.1", 0));
@@ -136,50 +136,43 @@ test "parse and render IPv4 addresses" {
try testing.expectError(error.NonCanonical, net.IpAddress.parseIp4("127.01.0.1", 0));
}
-test "parse and render UNIX addresses" {
- if (builtin.os.tag == .wasi) return error.SkipZigTest;
- if (!net.has_unix_sockets) return error.SkipZigTest;
-
- const addr = net.Address.initUnix("/tmp/testpath") catch unreachable;
- try std.testing.expectFmt("/tmp/testpath", "{f}", .{addr});
-
- const too_long = [_]u8{'a'} ** 200;
- try testing.expectError(error.NameTooLong, net.Address.initUnix(too_long[0..]));
-}
-
test "resolve DNS" {
if (builtin.os.tag == .wasi) return error.SkipZigTest;
- if (builtin.os.tag == .windows) {
- _ = try std.os.windows.WSAStartup(2, 2);
- }
- defer {
- if (builtin.os.tag == .windows) {
- std.os.windows.WSACleanup() catch unreachable;
- }
- }
+ const io = testing.io;
// Resolve localhost, this should not fail.
{
const localhost_v4 = try net.IpAddress.parse("127.0.0.1", 80);
const localhost_v6 = try net.IpAddress.parse("::2", 80);
- const result = try net.getAddressList(testing.allocator, "localhost", 80);
- defer result.deinit();
- for (result.addrs) |addr| {
- if (addr.eql(localhost_v4) or addr.eql(localhost_v6)) break;
+ var addresses_buffer: [8]net.IpAddress = undefined;
+ var canon_name_buffer: [net.HostName.max_len]u8 = undefined;
+ const result = try net.HostName.lookup(try .init("localhost"), io, .{
+ .port = 80,
+ .addresses_buffer = &addresses_buffer,
+ .canonical_name_buffer = &canon_name_buffer,
+ });
+ for (addresses_buffer[0..result.addresses_len]) |addr| {
+ if (addr.eql(&localhost_v4) or addr.eql(&localhost_v6)) break;
} else @panic("unexpected address for localhost");
}
{
// The tests are required to work even when there is no Internet connection,
// so some of these errors we must accept and skip the test.
- const result = net.getAddressList(testing.allocator, "example.com", 80) catch |err| switch (err) {
+ var addresses_buffer: [8]net.IpAddress = undefined;
+ var canon_name_buffer: [net.HostName.max_len]u8 = undefined;
+ const result = net.HostName.lookup(try .init("example.com"), io, .{
+ .port = 80,
+ .addresses_buffer = &addresses_buffer,
+ .canonical_name_buffer = &canon_name_buffer,
+ }) catch |err| switch (err) {
error.UnknownHostName => return error.SkipZigTest,
- error.TemporaryNameServerFailure => return error.SkipZigTest,
+ error.NameServerFailure => return error.SkipZigTest,
else => return err,
};
- result.deinit();
+ _ = result;
}
}
@@ -187,6 +180,8 @@ test "listen on a port, send bytes, receive bytes" {
if (builtin.single_threaded) return error.SkipZigTest;
if (builtin.os.tag == .wasi) return error.SkipZigTest;
+ const io = testing.io;
+
if (builtin.os.tag == .windows) {
_ = try std.os.windows.WSAStartup(2, 2);
}
@@ -198,28 +193,28 @@ test "listen on a port, send bytes, receive bytes" {
// Try only the IPv4 variant as some CI builders have no IPv6 localhost
// configured.
- const localhost = try net.IpAddress.parse("127.0.0.1", 0);
+ const localhost: net.IpAddress = .{ .ip4 = .loopback(0) };
- var server = try localhost.listen(.{});
- defer server.deinit();
+ var server = try localhost.listen(io, .{});
+ defer server.deinit(io);
const S = struct {
fn clientFn(server_address: net.IpAddress) !void {
- const socket = try net.tcpConnectToAddress(server_address);
- defer socket.close();
+ var stream = try server_address.connect(io, .{ .mode = .stream });
+ defer stream.close(io);
- var stream_writer = socket.writer(&.{});
+ var stream_writer = stream.writer(io, &.{});
try stream_writer.interface.writeAll("Hello world!");
}
};
- const t = try std.Thread.spawn(.{}, S.clientFn, .{server.listen_address});
+ const t = try std.Thread.spawn(.{}, S.clientFn, .{server.socket.address});
defer t.join();
- var client = try server.accept();
- defer client.stream.close();
+ var client = try server.accept(io);
+ defer client.stream.close(io);
var buf: [16]u8 = undefined;
- var stream_reader = client.stream.reader(&.{});
+ var stream_reader = client.stream.reader(io, &.{});
const n = try stream_reader.interface().readSliceShort(&buf);
try testing.expectEqual(@as(usize, 12), n);
@@ -232,13 +227,15 @@ test "listen on an in use port" {
return error.SkipZigTest;
}
- const localhost = try net.IpAddress.parse("127.0.0.1", 0);
+ const io = testing.io;
+
+ const localhost: net.IpAddress = .{ .ip4 = .loopback(0) };
- var server1 = try localhost.listen(.{ .reuse_address = true });
- defer server1.deinit();
+ var server1 = try localhost.listen(io, .{ .reuse_address = true });
+ defer server1.deinit(io);
- var server2 = try server1.listen_address.listen(.{ .reuse_address = true });
- defer server2.deinit();
+ var server2 = try server1.socket.address.listen(io, .{ .reuse_address = true });
+ defer server2.deinit(io);
}
fn testClientToHost(allocator: mem.Allocator, name: []const u8, port: u16) anyerror!void {
@@ -268,9 +265,11 @@ fn testClient(addr: net.IpAddress) anyerror!void {
fn testServer(server: *net.Server) anyerror!void {
if (builtin.os.tag == .wasi) return error.SkipZigTest;
- var client = try server.accept();
+ const io = testing.io;
+
+ var client = try server.accept(io);
- const stream = client.stream.writer();
+ const stream = client.stream.writer(io);
try stream.print("hello from server\n", .{});
}
@@ -278,6 +277,8 @@ test "listen on a unix socket, send bytes, receive bytes" {
if (builtin.single_threaded) return error.SkipZigTest;
if (!net.has_unix_sockets) return error.SkipZigTest;
+ const io = testing.io;
+
if (builtin.os.tag == .windows) {
_ = try std.os.windows.WSAStartup(2, 2);
}
@@ -293,15 +294,15 @@ test "listen on a unix socket, send bytes, receive bytes" {
const socket_addr = try net.IpAddress.initUnix(socket_path);
defer std.fs.cwd().deleteFile(socket_path) catch {};
- var server = try socket_addr.listen(.{});
- defer server.deinit();
+ var server = try socket_addr.listen(io, .{});
+ defer server.deinit(io);
const S = struct {
fn clientFn(path: []const u8) !void {
- const socket = try net.connectUnixSocket(path);
- defer socket.close();
+ var stream = try net.connectUnixSocket(path);
+ defer stream.close(io);
- var stream_writer = socket.writer(&.{});
+ var stream_writer = stream.writer(io, &.{});
try stream_writer.interface.writeAll("Hello world!");
}
};
@@ -309,10 +310,10 @@ test "listen on a unix socket, send bytes, receive bytes" {
const t = try std.Thread.spawn(.{}, S.clientFn, .{socket_path});
defer t.join();
- var client = try server.accept();
- defer client.stream.close();
+ var client = try server.accept(io);
+ defer client.stream.close(io);
var buf: [16]u8 = undefined;
- var stream_reader = client.stream.reader(&.{});
+ var stream_reader = client.stream.reader(io, &.{});
const n = try stream_reader.interface().readSliceShort(&buf);
try testing.expectEqual(@as(usize, 12), n);
@@ -324,14 +325,16 @@ test "listen on a unix socket with reuse_address option" {
// Windows doesn't implement reuse port option.
if (builtin.os.tag == .windows) return error.SkipZigTest;
+ const io = testing.io;
+
const socket_path = try generateFileName("socket.unix");
defer testing.allocator.free(socket_path);
const socket_addr = try net.Address.initUnix(socket_path);
defer std.fs.cwd().deleteFile(socket_path) catch {};
- var server = try socket_addr.listen(.{ .reuse_address = true });
- server.deinit();
+ var server = try socket_addr.listen(io, .{ .reuse_address = true });
+ server.deinit(io);
}
fn generateFileName(base_name: []const u8) ![]const u8 {
@@ -351,19 +354,21 @@ test "non-blocking tcp server" {
return error.SkipZigTest;
}
- const localhost = try net.IpAddress.parse("127.0.0.1", 0);
- var server = localhost.listen(.{ .force_nonblocking = true });
- defer server.deinit();
+ const io = testing.io;
+
+ const localhost: net.IpAddress = .{ .ip4 = .loopback(0) };
+ var server = localhost.listen(io, .{ .force_nonblocking = true });
+ defer server.deinit(io);
- const accept_err = server.accept();
+ const accept_err = server.accept(io);
try testing.expectError(error.WouldBlock, accept_err);
- const socket_file = try net.tcpConnectToAddress(server.listen_address);
+ const socket_file = try net.tcpConnectToAddress(server.socket.address);
defer socket_file.close();
- var client = try server.accept();
- defer client.stream.close();
- const stream = client.stream.writer();
+ var client = try server.accept(io);
+ defer client.stream.close(io);
+ const stream = client.stream.writer(io);
try stream.print("hello from server\n", .{});
var buf: [100]u8 = undefined;
lib/std/Io/File.zig
@@ -319,6 +319,11 @@ pub const Reader = struct {
};
}
+ /// Takes a legacy `std.fs.File` to help with upgrading.
+ pub fn initAdapted(file: std.fs.File, io: Io, buffer: []u8) Reader {
+ return .init(.{ .handle = file.handle }, io, buffer);
+ }
+
pub fn initSize(file: File, io: Io, buffer: []u8, size: ?u64) Reader {
return .{
.io = io,
lib/std/Io/net.zig
@@ -186,7 +186,7 @@ pub const IpAddress = union(enum) {
/// Waits for a TCP connection. When using this API, `bind` does not need
/// to be called. The returned `Server` has an open `stream`.
pub fn listen(address: IpAddress, io: Io, options: ListenOptions) ListenError!Server {
- return io.vtable.tcpListen(io.userdata, address, options);
+ return io.vtable.listen(io.userdata, address, options);
}
pub const BindError = error{
@@ -236,6 +236,8 @@ pub const IpAddress = union(enum) {
AddressInUse,
AddressUnavailable,
AddressFamilyUnsupported,
+ /// Insufficient memory or other resource internal to the operating system.
+ SystemResources,
ConnectionPending,
ConnectionRefused,
ConnectionResetByPeer,
@@ -246,12 +248,23 @@ pub const IpAddress = union(enum) {
/// One of the `ConnectOptions` is not supported by the Io
/// implementation.
OptionUnsupported,
- } || Io.UnexpectedError || Io.Cancelable;
+ /// Per-process limit on the number of open file descriptors has been reached.
+ ProcessFdQuotaExceeded,
+ /// System-wide limit on the total number of open files has been reached.
+ SystemFdQuotaExceeded,
+ ProtocolUnsupportedBySystem,
+ ProtocolUnsupportedByAddressFamily,
+ SocketModeUnsupported,
+ } || Io.Timeout.Error || Io.UnexpectedError || Io.Cancelable;
- pub const ConnectOptions = BindOptions;
+ pub const ConnectOptions = struct {
+ mode: Socket.Mode,
+ protocol: ?Protocol = null,
+ timeout: Io.Timeout = .none,
+ };
/// Initiates a connection-oriented network stream.
- pub fn connect(address: IpAddress, io: Io, options: ConnectOptions) ConnectError!Stream {
+ pub fn connect(address: *const IpAddress, io: Io, options: ConnectOptions) ConnectError!Stream {
return io.vtable.ipConnect(io.userdata, address, options);
}
};
@@ -997,7 +1010,7 @@ pub const Stream = struct {
socket: Socket,
pub fn close(s: *Stream, io: Io) void {
- io.vtable.netClose(io.userdata, s.socket);
+ io.vtable.netClose(io.userdata, s.socket.handle);
s.* = undefined;
}
@@ -1040,10 +1053,13 @@ pub const Stream = struct {
return n;
}
- fn readVec(io_r: *Reader, data: [][]u8) Io.Reader.Error!usize {
+ fn readVec(io_r: *Io.Reader, data: [][]u8) Io.Reader.Error!usize {
const r: *Reader = @alignCast(@fieldParentPtr("interface", io_r));
const io = r.io;
- return io.vtable.netReadVec(io.vtable.userdata, r.stream, io_r, data);
+ return io.vtable.netRead(io.userdata, r.stream, data) catch |err| {
+ r.err = err;
+ return error.ReadFailed;
+ };
}
};
@@ -1078,7 +1094,10 @@ pub const Stream = struct {
const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
const io = w.io;
const buffered = io_w.buffered();
- const n = try io.vtable.netWrite(io.vtable.userdata, w.stream, buffered, data, splat);
+ const n = io.vtable.netWrite(io.userdata, w.stream, buffered, data, splat) catch |err| {
+ w.err = err;
+ return error.WriteFailed;
+ };
return io_w.consume(n);
}
};
@@ -1104,7 +1123,7 @@ pub const Server = struct {
/// Blocks until a client connects to the server.
pub fn accept(s: *Server, io: Io) AcceptError!Stream {
- return io.vtable.accept(io, s);
+ return io.vtable.accept(io.userdata, s);
}
};
lib/std/Io/Threaded.zig
@@ -1032,7 +1032,7 @@ fn nowWindows(userdata: ?*anyopaque, clock: Io.Timestamp.Clock) Io.Timestamp.Err
// and uses the NTFS/Windows epoch, which is 1601-01-01.
return @as(i96, windows.ntdll.RtlGetSystemTimePrecise()) * 100;
},
- .monotonic, .boottime => {
+ .monotonic, .uptime => {
// QPC on windows doesn't fail on >= XP/2000 and includes time suspended.
return .{ .timestamp = windows.QueryPerformanceCounter() };
},
@@ -1132,7 +1132,8 @@ fn sleepPosix(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {
.sec = std.math.maxInt(sec_type),
.nsec = std.math.maxInt(nsec_type),
};
- if (d.clock != .monotonic) return error.UnsupportedClock;
+ // TODO check which clock nanosleep uses on this host
+ // and return error.UnsupportedClock if it does not match
const ns = d.duration.nanoseconds;
break :t .{
.sec = @intCast(@divFloor(ns, std.time.ns_per_s)),
@@ -1331,11 +1332,15 @@ fn setSocketOption(pool: *Pool, fd: posix.fd_t, level: i32, opt_name: u32, optio
fn ipConnectPosix(
userdata: ?*anyopaque,
address: *const Io.net.IpAddress,
- options: Io.net.IpAddress.BindOptions,
+ options: Io.net.IpAddress.ConnectOptions,
) Io.net.IpAddress.ConnectError!Io.net.Stream {
+ if (options.timeout != .none) @panic("TODO");
const pool: *Pool = @ptrCast(@alignCast(userdata));
const family = posixAddressFamily(address);
- const socket_fd = try openSocketPosix(pool, family, options);
+ const socket_fd = try openSocketPosix(pool, family, .{
+ .mode = options.mode,
+ .protocol = options.protocol,
+ });
var storage: PosixAddress = undefined;
var addr_len = addressToPosix(address, &storage);
try posixConnect(pool, socket_fd, &storage.any, addr_len);
@@ -1490,11 +1495,11 @@ fn netSend(
const pool: *Pool = @ptrCast(@alignCast(userdata));
const posix_flags: u32 =
- @as(u32, if (flags.confirm) posix.MSG.CONFIRM else 0) |
+ @as(u32, if (@hasDecl(posix.MSG, "CONFIRM") and flags.confirm) posix.MSG.CONFIRM else 0) |
@as(u32, if (flags.dont_route) posix.MSG.DONTROUTE else 0) |
@as(u32, if (flags.eor) posix.MSG.EOR else 0) |
@as(u32, if (flags.oob) posix.MSG.OOB else 0) |
- @as(u32, if (flags.fastopen) posix.MSG.FASTOPEN else 0) |
+ @as(u32, if (@hasDecl(posix.MSG, "FASTOPEN") and flags.fastopen) posix.MSG.FASTOPEN else 0) |
posix.MSG.NOSIGNAL;
var i: usize = 0;
@@ -2024,11 +2029,17 @@ fn recoverableOsBugDetected() void {
fn clockToPosix(clock: Io.Timestamp.Clock) posix.clockid_t {
return switch (clock) {
- .realtime => posix.CLOCK.REALTIME,
- .monotonic => posix.CLOCK.MONOTONIC,
- .boottime => posix.CLOCK.BOOTTIME,
- .process_cputime_id => posix.CLOCK.PROCESS_CPUTIME_ID,
- .thread_cputime_id => posix.CLOCK.THREAD_CPUTIME_ID,
+ .real => posix.CLOCK.REALTIME,
+ .awake => switch (builtin.os.tag) {
+ .macos, .ios, .watchos, .tvos => posix.CLOCK.UPTIME_RAW,
+ else => posix.CLOCK.MONOTONIC,
+ },
+ .boot => switch (builtin.os.tag) {
+ .macos, .ios, .watchos, .tvos => posix.CLOCK.MONOTONIC_RAW,
+ else => posix.CLOCK.BOOTTIME,
+ },
+ .cpu_process => posix.CLOCK.PROCESS_CPUTIME_ID,
+ .cpu_thread => posix.CLOCK.THREAD_CPUTIME_ID,
};
}
@@ -2036,7 +2047,7 @@ fn clockToWasi(clock: Io.Timestamp.Clock) std.os.wasi.clockid_t {
return switch (clock) {
.realtime => .REALTIME,
.monotonic => .MONOTONIC,
- .boottime => .MONOTONIC,
+ .uptime => .MONOTONIC,
.process_cputime_id => .PROCESS_CPUTIME_ID,
.thread_cputime_id => .THREAD_CPUTIME_ID,
};
lib/std/Io.zig
@@ -719,31 +719,38 @@ pub const Timestamp = struct {
///
/// The epoch is implementation-defined. For example NTFS/Windows uses
/// 1601-01-01.
- realtime,
+ real,
/// A nonsettable system-wide clock that represents time since some
/// unspecified point in the past.
///
- /// On Linux, corresponds to how long the system has been running since
- /// it booted.
+ /// Monotonic: Guarantees that the time returned by consecutive calls
+ /// will not go backwards, but successive calls may return identical
+ /// (not-increased) time values.
///
/// Not affected by discontinuous jumps in the system time (e.g., if
- /// the system administrator manually changes the clock), but is
- /// affected by frequency adjustments. **This clock does not count time
- /// that the system is suspended.**
+ /// the system administrator manually changes the clock), but may be
+ /// affected by frequency adjustments.
///
- /// Guarantees that the time returned by consecutive calls will not go
- /// backwards, but successive calls may return identical
- /// (not-increased) time values.
+ /// This clock expresses intent to **exclude time that the system is
+ /// suspended**. However, implementations may be unable to satisify
+ /// this, and may include that time.
+ ///
+ /// * On Linux, corresponds `CLOCK_MONOTONIC`.
+ /// * On macOS, corresponds to `CLOCK_UPTIME_RAW`.
+ awake,
+ /// Identical to `awake` except it expresses intent to include time
+ /// that the system is suspended, however, it may be implemented
+ /// identically to `awake`.
///
- /// May or may not include time the system is suspended, but
- /// implementations should exclude that time if possible.
- monotonic,
- /// Identical to `monotonic` except it also includes any time that the
- /// system is suspended, if possible. However, it may be implemented
- /// identically to `monotonic`.
- boottime,
- process_cputime_id,
- thread_cputime_id,
+ /// * On Linux, corresponds `CLOCK_BOOTTIME`.
+ /// * On macOS, corresponds to `CLOCK_MONOTONIC_RAW`.
+ boot,
+ /// Tracks the amount of CPU in user or kernel mode used by the calling
+ /// process.
+ cpu_process,
+ /// Tracks the amount of CPU in user or kernel mode used by the calling
+ /// thread.
+ cpu_thread,
};
pub fn durationTo(from: Timestamp, to: Timestamp) Duration {
@@ -825,7 +832,7 @@ pub const Duration = struct {
}
pub fn sleep(duration: Duration, io: Io) SleepError!void {
- return io.vtable.sleep(io.userdata, .{ .duration = .{ .duration = duration, .clock = .monotonic } });
+ return io.vtable.sleep(io.userdata, .{ .duration = .{ .duration = duration, .clock = .awake } });
}
};
lib/std/Uri.zig
@@ -1,45 +1,48 @@
-//! Uniform Resource Identifier (URI) parsing roughly adhering to <https://tools.ietf.org/html/rfc3986>.
-//! Does not do perfect grammar and character class checking, but should be robust against URIs in the wild.
+//! Uniform Resource Identifier (URI) parsing roughly adhering to
+//! <https://tools.ietf.org/html/rfc3986>. Does not do perfect grammar and
+//! character class checking, but should be robust against URIs in the wild.
const std = @import("std.zig");
const testing = std.testing;
const Uri = @This();
const Allocator = std.mem.Allocator;
const Writer = std.Io.Writer;
+const HostName = std.Io.net.HostName;
scheme: []const u8,
user: ?Component = null,
password: ?Component = null,
+/// If non-null, already validated.
host: ?Component = null,
port: ?u16 = null,
path: Component = Component.empty,
query: ?Component = null,
fragment: ?Component = null,
-pub const host_name_max = 255;
+pub const GetHostError = error{UriMissingHost};
/// Returned value may point into `buffer` or be the original string.
///
-/// Suggested buffer length: `host_name_max`.
-///
/// See also:
/// * `getHostAlloc`
-pub fn getHost(uri: Uri, buffer: []u8) error{ UriMissingHost, UriHostTooLong }![]const u8 {
+pub fn getHost(uri: Uri, buffer: *[HostName.max_len]u8) GetHostError!HostName {
const component = uri.host orelse return error.UriMissingHost;
- return component.toRaw(buffer) catch |err| switch (err) {
- error.NoSpaceLeft => return error.UriHostTooLong,
+ const bytes = component.toRaw(buffer) catch |err| switch (err) {
+ error.NoSpaceLeft => unreachable, // `host` already validated.
};
+ return .{ .bytes = bytes };
}
+pub const GetHostAllocError = GetHostError || error{OutOfMemory};
+
/// Returned value may point into `buffer` or be the original string.
///
/// See also:
/// * `getHost`
-pub fn getHostAlloc(uri: Uri, arena: Allocator) error{ UriMissingHost, UriHostTooLong, OutOfMemory }![]const u8 {
+pub fn getHostAlloc(uri: Uri, arena: Allocator) GetHostAllocError![]const u8 {
const component = uri.host orelse return error.UriMissingHost;
- const result = try component.toRawMaybeAlloc(arena);
- if (result.len > host_name_max) return error.UriHostTooLong;
- return result;
+ const bytes = try component.toRawMaybeAlloc(arena);
+ return .{ .bytes = bytes };
}
pub const Component = union(enum) {
@@ -397,7 +400,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE
.scheme = new_parsed.scheme,
.user = new_parsed.user,
.password = new_parsed.password,
- .host = new_parsed.host,
+ .host = try validateHost(new_parsed.host),
.port = new_parsed.port,
.path = remove_dot_segments(new_path),
.query = new_parsed.query,
@@ -408,7 +411,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE
.scheme = base.scheme,
.user = new_parsed.user,
.password = new_parsed.password,
- .host = host,
+ .host = try validateHost(host),
.port = new_parsed.port,
.path = remove_dot_segments(new_path),
.query = new_parsed.query,
@@ -430,7 +433,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE
.scheme = base.scheme,
.user = base.user,
.password = base.password,
- .host = base.host,
+ .host = try validateHost(base.host),
.port = base.port,
.path = path,
.query = query,
@@ -438,6 +441,11 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE
};
}
+fn validateHost(bytes: []const u8) []const u8 {
+ try HostName.validate(bytes);
+ return bytes;
+}
+
/// In-place implementation of RFC 3986, Section 5.2.4.
fn remove_dot_segments(path: []u8) Component {
var in_i: usize = 0;