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,
1919
2020pub 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
2234pub const ValidateError = error{
2335 NameTooLong,
2436 InvalidHostName,
......@@ -28,11 +40,11 @@ pub const ValidateError = error{
2840pub fn validate(bytes: []const u8) ValidateError!void {
2941 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
3443 // 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
3749 // Hostnames are divided into dot-separated "labels", which:
3850 //
......@@ -83,7 +95,6 @@ test validate {
8395 const many_a_dot_buf: [127][2]u8 = @splat(.{ 'a', '.' });
8496 const many_a_dot: []const u8 = @ptrCast(&many_a_dot_buf);
8597 try validate(many_a_dot ++ "a"); // Total length 255 (valid)
86 try validate(many_a_dot ++ "a."); // Total length 255 + trailing dot (valid)
8798
8899 // Invalid hostnames
89100 try std.testing.expectError(error.InvalidHostName, validate(""));
......@@ -98,8 +109,8 @@ test validate {
98109 try std.testing.expectError(error.InvalidHostName, validate("."));
99110 try std.testing.expectError(error.InvalidHostName, validate(".."));
100111 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)
101113 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)
103114}
104115
105116pub fn init(bytes: []const u8) ValidateError!HostName {
lib/std/Uri.zig+5-42
......@@ -7,43 +7,18 @@ const testing = std.testing;
77const Uri = @This();
88const Allocator = std.mem.Allocator;
99const Writer = std.Io.Writer;
10const HostName = std.Io.net.HostName;
1110
1211scheme: []const u8,
1312user: ?Component = null,
1413password: ?Component = null,
15/// If non-null, already validated.
1614host: ?Component = null,
1715port: ?u16 = null,
1816path: Component = Component.empty,
1917query: ?Component = null,
2018fragment: ?Component = null,
2119
22pub const GetHostError = error{UriMissingHost};
23
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}
20pub const getHost = @compileError("This function has been moved to std.Io.net.HostName.fromUri");
21pub const getHostAlloc = @compileError("This function has been deleted. See std.Io.net.HostName.fromUri instead");
4722
4823pub const Component = union(enum) {
4924 /// 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
400375 .scheme = new_parsed.scheme,
401376 .user = new_parsed.user,
402377 .password = new_parsed.password,
403 .host = try validateHostComponent(new_parsed.host),
378 .host = new_parsed.host,
404379 .port = new_parsed.port,
405380 .path = remove_dot_segments(new_path),
406381 .query = new_parsed.query,
......@@ -411,7 +386,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE
411386 .scheme = base.scheme,
412387 .user = new_parsed.user,
413388 .password = new_parsed.password,
414 .host = try validateHostComponent(host),
389 .host = host,
415390 .port = new_parsed.port,
416391 .path = remove_dot_segments(new_path),
417392 .query = new_parsed.query,
......@@ -433,7 +408,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE
433408 .scheme = base.scheme,
434409 .user = base.user,
435410 .password = base.password,
436 .host = try validateHostComponent(base.host),
411 .host = base.host,
437412 .port = base.port,
438413 .path = path,
439414 .query = query,
......@@ -441,18 +416,6 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE
441416 };
442417}
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
456419/// In-place implementation of RFC 3986, Section 5.2.4.
457420fn remove_dot_segments(path: []u8) Component {
458421 var in_i: usize = 0;
lib/std/http/Client.zig+13-4
......@@ -1229,7 +1229,11 @@ pub const Request = struct {
12291229 const old_connection = r.connection.?;
12301230 const old_host = old_connection.host();
12311231 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 };
12331237 const keep_privileged_headers =
12341238 std.ascii.eqlIgnoreCase(r.uri.scheme, new_uri.scheme) and
12351239 old_host.sameParentDomain(new_host);
......@@ -1349,7 +1353,8 @@ fn createProxyFromEnvVar(
13491353
13501354 const uri = Uri.parse(content) catch try Uri.parseAfterScheme("http", content);
13511355 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
13541359 const authorization: ?[]const u8 = if (uri.user != null or uri.password != null) a: {
13551360 const authorization = try arena.alloc(u8, basic_authorization.valueLengthFromUri(uri));
......@@ -1360,7 +1365,7 @@ fn createProxyFromEnvVar(
13601365 const proxy = try arena.create(Proxy);
13611366 proxy.* = .{
13621367 .protocol = protocol,
1363 .host = raw_host,
1368 .host = .{ .bytes = try arena.dupe(u8, raw_host.bytes) },
13641369 .authorization = authorization,
13651370 .port = uriPort(uri, protocol),
13661371 .supports_connect = true,
......@@ -1624,6 +1629,7 @@ pub fn connect(
16241629pub const RequestError = ConnectTcpError || error{
16251630 UnsupportedUriScheme,
16261631 UriMissingHost,
1632 InvalidHostName,
16271633 CertificateBundleLoadFailure,
16281634};
16291635
......@@ -1721,7 +1727,10 @@ pub fn request(
17211727
17221728 const connection = options.connection orelse c: {
17231729 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 };
17251734 break :c try client.connect(host_name, uriPort(uri, protocol), protocol);
17261735 };
17271736