authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2026-07-11 00:06:19+02:00
committergravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2026-07-11 00:06:19+02:00
log7796e5cdffd52073b56f6b9176dc0b99dedb62ef
tree50b49c9bbdcb43879feaebf9daba4608dcc66bf9
parente07edda6e796c3488af178c5c1754dfc766a9391
parentb529a94e14725ea5f56ab4f48de0801c9f7e0b8b

Merge pull request 'Decouple Uri and net.HostName, plus a max len-related fix' (#36036) from squeek502/zig:uri-and-hostname into master

Reviewed-on: https://codeberg.org/ziglang/zig/pulls/36036

3 files changed, 35 insertions(+), 52 deletions(-)

lib/std/Io/net/HostName.zig+17-6
...@@ -19,6 +19,18 @@ bytes: []const u8,...@@ -19,6 +19,18 @@ bytes: []const u8,
1919
20pub const max_len = 255;20pub const max_len = 255;
2121
22pub const FromUriError = error{UriMissingHost} || ValidateError;
23
24/// Returned `HostName.bytes` may point into `buffer` or `uri.host`.
25pub fn fromUri(uri: std.Uri, buffer: *[HostName.max_len]u8) FromUriError!HostName {
26 const component = uri.host orelse return error.UriMissingHost;
27 const bytes = component.toRaw(buffer) catch |err| switch (err) {
28 error.NoSpaceLeft => return error.NameTooLong,
29 };
30 try validate(bytes);
31 return .{ .bytes = bytes };
32}
33
22pub const ValidateError = error{34pub const ValidateError = error{
23 NameTooLong,35 NameTooLong,
24 InvalidHostName,36 InvalidHostName,
...@@ -28,11 +40,11 @@ pub const ValidateError = error{...@@ -28,11 +40,11 @@ pub const ValidateError = error{
28pub fn validate(bytes: []const u8) ValidateError!void {40pub fn validate(bytes: []const u8) ValidateError!void {
29 if (bytes.len == 0) return error.InvalidHostName;41 if (bytes.len == 0) return error.InvalidHostName;
3042
31 // Ignore trailing dot (FQDN). It doesn't count toward our length.
32 const end = if (bytes[bytes.len - 1] == '.') bytes.len - 1 else bytes.len;
33
34 // The accepted maximum length of a hostname, including labels and dots.43 // The accepted maximum length of a hostname, including labels and dots.
35 if (end > max_len) return error.NameTooLong;44 if (bytes.len > max_len) return error.NameTooLong;
45
46 // Ignore trailing dot (FQDN).
47 const end = if (bytes[bytes.len - 1] == '.') bytes.len - 1 else bytes.len;
3648
37 // Hostnames are divided into dot-separated "labels", which:49 // Hostnames are divided into dot-separated "labels", which:
38 //50 //
...@@ -83,7 +95,6 @@ test validate {...@@ -83,7 +95,6 @@ test validate {
83 const many_a_dot_buf: [127][2]u8 = @splat(.{ 'a', '.' });95 const many_a_dot_buf: [127][2]u8 = @splat(.{ 'a', '.' });
84 const many_a_dot: []const u8 = @ptrCast(&many_a_dot_buf);96 const many_a_dot: []const u8 = @ptrCast(&many_a_dot_buf);
85 try validate(many_a_dot ++ "a"); // Total length 255 (valid)97 try validate(many_a_dot ++ "a"); // Total length 255 (valid)
86 try validate(many_a_dot ++ "a."); // Total length 255 + trailing dot (valid)
8798
88 // Invalid hostnames99 // Invalid hostnames
89 try std.testing.expectError(error.InvalidHostName, validate(""));100 try std.testing.expectError(error.InvalidHostName, validate(""));
...@@ -98,8 +109,8 @@ test validate {...@@ -98,8 +109,8 @@ test validate {
98 try std.testing.expectError(error.InvalidHostName, validate("."));109 try std.testing.expectError(error.InvalidHostName, validate("."));
99 try std.testing.expectError(error.InvalidHostName, validate(".."));110 try std.testing.expectError(error.InvalidHostName, validate(".."));
100 try std.testing.expectError(error.InvalidHostName, validate(&many_a ++ "a.com")); // Label length 64 (too long)111 try std.testing.expectError(error.InvalidHostName, validate(&many_a ++ "a.com")); // Label length 64 (too long)
112 try std.testing.expectError(error.NameTooLong, validate(many_a_dot ++ "a.")); // Total length 255 + trailing dot (too long)
101 try std.testing.expectError(error.NameTooLong, validate(many_a_dot ++ "ab")); // Total length 256 (too long)113 try std.testing.expectError(error.NameTooLong, validate(many_a_dot ++ "ab")); // Total length 256 (too long)
102 try std.testing.expectError(error.NameTooLong, validate(many_a_dot ++ "ab.")); // Total length 256 + trailing dot (too long)
103}114}
104115
105pub fn init(bytes: []const u8) ValidateError!HostName {116pub fn init(bytes: []const u8) ValidateError!HostName {
lib/std/Uri.zig+5-42
...@@ -7,43 +7,18 @@ const testing = std.testing;...@@ -7,43 +7,18 @@ const testing = std.testing;
7const Uri = @This();7const Uri = @This();
8const Allocator = std.mem.Allocator;8const Allocator = std.mem.Allocator;
9const Writer = std.Io.Writer;9const Writer = std.Io.Writer;
10const HostName = std.Io.net.HostName;
1110
12scheme: []const u8,11scheme: []const u8,
13user: ?Component = null,12user: ?Component = null,
14password: ?Component = null,13password: ?Component = null,
15/// If non-null, already validated.
16host: ?Component = null,14host: ?Component = null,
17port: ?u16 = null,15port: ?u16 = null,
18path: Component = Component.empty,16path: Component = Component.empty,
19query: ?Component = null,17query: ?Component = null,
20fragment: ?Component = null,18fragment: ?Component = null,
2119
22pub const GetHostError = error{UriMissingHost};20pub const getHost = @compileError("This function has been moved to std.Io.net.HostName.fromUri");
2321pub const getHostAlloc = @compileError("This function has been deleted. See std.Io.net.HostName.fromUri instead");
24/// Returned value may point into `buffer` or be the original string.
25///
26/// See also:
27/// * `getHostAlloc`
28pub fn getHost(uri: Uri, buffer: *[HostName.max_len]u8) GetHostError!HostName {
29 const component = uri.host orelse return error.UriMissingHost;
30 const bytes = component.toRaw(buffer) catch |err| switch (err) {
31 error.NoSpaceLeft => unreachable, // `host` already validated.
32 };
33 return .{ .bytes = bytes };
34}
35
36pub const GetHostAllocError = GetHostError || error{OutOfMemory};
37
38/// Returned value may point into `buffer` or be the original string.
39///
40/// See also:
41/// * `getHost`
42pub fn getHostAlloc(uri: Uri, arena: Allocator) GetHostAllocError!HostName {
43 const component = uri.host orelse return error.UriMissingHost;
44 const bytes = try component.toRawMaybeAlloc(arena);
45 return .{ .bytes = bytes };
46}
4722
48pub const Component = union(enum) {23pub const Component = union(enum) {
49 /// Invalid characters in this component must be percent encoded24 /// Invalid characters in this component must be percent encoded
...@@ -400,7 +375,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE...@@ -400,7 +375,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE
400 .scheme = new_parsed.scheme,375 .scheme = new_parsed.scheme,
401 .user = new_parsed.user,376 .user = new_parsed.user,
402 .password = new_parsed.password,377 .password = new_parsed.password,
403 .host = try validateHostComponent(new_parsed.host),378 .host = new_parsed.host,
404 .port = new_parsed.port,379 .port = new_parsed.port,
405 .path = remove_dot_segments(new_path),380 .path = remove_dot_segments(new_path),
406 .query = new_parsed.query,381 .query = new_parsed.query,
...@@ -411,7 +386,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE...@@ -411,7 +386,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE
411 .scheme = base.scheme,386 .scheme = base.scheme,
412 .user = new_parsed.user,387 .user = new_parsed.user,
413 .password = new_parsed.password,388 .password = new_parsed.password,
414 .host = try validateHostComponent(host),389 .host = host,
415 .port = new_parsed.port,390 .port = new_parsed.port,
416 .path = remove_dot_segments(new_path),391 .path = remove_dot_segments(new_path),
417 .query = new_parsed.query,392 .query = new_parsed.query,
...@@ -433,7 +408,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE...@@ -433,7 +408,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE
433 .scheme = base.scheme,408 .scheme = base.scheme,
434 .user = base.user,409 .user = base.user,
435 .password = base.password,410 .password = base.password,
436 .host = try validateHostComponent(base.host),411 .host = base.host,
437 .port = base.port,412 .port = base.port,
438 .path = path,413 .path = path,
439 .query = query,414 .query = query,
...@@ -441,18 +416,6 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE...@@ -441,18 +416,6 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE
441 };416 };
442}417}
443418
444fn validateHostComponent(optional_component: ?Component) error{InvalidHostName}!?Component {
445 const component = optional_component orelse return null;
446 switch (component) {
447 .raw => |raw| HostName.validate(raw) catch return error.InvalidHostName,
448 .percent_encoded => |encoded| {
449 // TODO validate decoded name instead
450 HostName.validate(encoded) catch return error.InvalidHostName;
451 },
452 }
453 return component;
454}
455
456/// In-place implementation of RFC 3986, Section 5.2.4.419/// In-place implementation of RFC 3986, Section 5.2.4.
457fn remove_dot_segments(path: []u8) Component {420fn remove_dot_segments(path: []u8) Component {
458 var in_i: usize = 0;421 var in_i: usize = 0;
lib/std/http/Client.zig+13-4
...@@ -1229,7 +1229,11 @@ pub const Request = struct {...@@ -1229,7 +1229,11 @@ pub const Request = struct {
1229 const old_connection = r.connection.?;1229 const old_connection = r.connection.?;
1230 const old_host = old_connection.host();1230 const old_host = old_connection.host();
1231 var new_host_name_buffer: [HostName.max_len]u8 = undefined;1231 var new_host_name_buffer: [HostName.max_len]u8 = undefined;
1232 const new_host = try new_uri.getHost(&new_host_name_buffer);1232 const new_host = HostName.fromUri(new_uri, &new_host_name_buffer) catch |err| switch (err) {
1233 error.UriMissingHost => return error.HttpRedirectLocationInvalid,
1234 error.InvalidHostName => return error.HttpRedirectLocationInvalid,
1235 error.NameTooLong => return error.HttpRedirectLocationOversize,
1236 };
1233 const keep_privileged_headers =1237 const keep_privileged_headers =
1234 std.ascii.eqlIgnoreCase(r.uri.scheme, new_uri.scheme) and1238 std.ascii.eqlIgnoreCase(r.uri.scheme, new_uri.scheme) and
1235 old_host.sameParentDomain(new_host);1239 old_host.sameParentDomain(new_host);
...@@ -1349,7 +1353,8 @@ fn createProxyFromEnvVar(...@@ -1349,7 +1353,8 @@ fn createProxyFromEnvVar(
13491353
1350 const uri = Uri.parse(content) catch try Uri.parseAfterScheme("http", content);1354 const uri = Uri.parse(content) catch try Uri.parseAfterScheme("http", content);
1351 const protocol = Protocol.fromUri(uri) orelse return null;1355 const protocol = Protocol.fromUri(uri) orelse return null;
1352 const raw_host = try uri.getHostAlloc(arena);1356 var host_buf: [HostName.max_len]u8 = undefined;
1357 const raw_host = try HostName.fromUri(uri, &host_buf);
13531358
1354 const authorization: ?[]const u8 = if (uri.user != null or uri.password != null) a: {1359 const authorization: ?[]const u8 = if (uri.user != null or uri.password != null) a: {
1355 const authorization = try arena.alloc(u8, basic_authorization.valueLengthFromUri(uri));1360 const authorization = try arena.alloc(u8, basic_authorization.valueLengthFromUri(uri));
...@@ -1360,7 +1365,7 @@ fn createProxyFromEnvVar(...@@ -1360,7 +1365,7 @@ fn createProxyFromEnvVar(
1360 const proxy = try arena.create(Proxy);1365 const proxy = try arena.create(Proxy);
1361 proxy.* = .{1366 proxy.* = .{
1362 .protocol = protocol,1367 .protocol = protocol,
1363 .host = raw_host,1368 .host = .{ .bytes = try arena.dupe(u8, raw_host.bytes) },
1364 .authorization = authorization,1369 .authorization = authorization,
1365 .port = uriPort(uri, protocol),1370 .port = uriPort(uri, protocol),
1366 .supports_connect = true,1371 .supports_connect = true,
...@@ -1624,6 +1629,7 @@ pub fn connect(...@@ -1624,6 +1629,7 @@ pub fn connect(
1624pub const RequestError = ConnectTcpError || error{1629pub const RequestError = ConnectTcpError || error{
1625 UnsupportedUriScheme,1630 UnsupportedUriScheme,
1626 UriMissingHost,1631 UriMissingHost,
1632 InvalidHostName,
1627 CertificateBundleLoadFailure,1633 CertificateBundleLoadFailure,
1628};1634};
16291635
...@@ -1721,7 +1727,10 @@ pub fn request(...@@ -1721,7 +1727,10 @@ pub fn request(
17211727
1722 const connection = options.connection orelse c: {1728 const connection = options.connection orelse c: {
1723 var host_name_buffer: [HostName.max_len]u8 = undefined;1729 var host_name_buffer: [HostName.max_len]u8 = undefined;
1724 const host_name = try uri.getHost(&host_name_buffer);1730 const host_name = HostName.fromUri(uri, &host_name_buffer) catch |err| switch (err) {
1731 error.UriMissingHost => |e| return e,
1732 error.NameTooLong, error.InvalidHostName => return error.InvalidHostName,
1733 };
1725 break :c try client.connect(host_name, uriPort(uri, protocol), protocol);1734 break :c try client.connect(host_name, uriPort(uri, protocol), protocol);
1726 };1735 };
17271736