// SPDX-FileCopyrightText: © 2026 Francesc Muñoz // SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! Socket addresses as the tools print them: `192.0.2.1:443`, `[fe80::1]:5353`, //! `*:5353`. They are kept as the strings they arrived as, since that is also //! what is drawn, and taken apart only where a part is needed. const std = @import("std"); const z46 = @import("z46"); pub const HostPort = struct { ip: []const u8, port: []const u8, }; pub fn splitHostPort(addr: []const u8) HostPort { if (addr.len > 0 and addr[0] == '[') { const end = std.mem.findScalar(u8, addr, ']') orelse return .{ .ip = addr[1..], .port = "" }; return .{ .ip = addr[1..end], .port = if (end + 2 <= addr.len) addr[end + 2 ..] else "" }; } const sep = std.mem.findScalarLast(u8, addr, ':') orelse return .{ .ip = addr, .port = "" }; return .{ .ip = addr[0..sep], .port = addr[sep + 1 ..] }; } /// The remote port of an endpoint, or null when it has none worth the name. /// The last colon is the separator, which also does the right thing for a /// bracketed IPv6 address. pub fn port(remote: []const u8) ?u16 { const i = std.mem.findScalarLast(u8, remote, ':') orelse return null; const p = std.fmt.parseInt(u16, remote[i + 1 ..], 10) catch return null; return if (p > 0) p else null; } /// Whether an address is one only this network can reach, which is to say /// one there is no point asking anyone on the Internet about. pub fn isPrivate(ip: []const u8) bool { const address = z46.Address.parse(ip) catch return false; return address.isPrivate() or address.isLoopback() or address.isLinkLocal(); } const testing = std.testing; test "splitHostPort" { try testing.expectEqualDeep(HostPort{ .ip = "192.0.2.1", .port = "443" }, splitHostPort("192.0.2.1:443")); try testing.expectEqualDeep(HostPort{ .ip = "fe80::1", .port = "5353" }, splitHostPort("[fe80::1]:5353")); try testing.expectEqualDeep(HostPort{ .ip = "192.0.2.1", .port = "" }, splitHostPort("192.0.2.1")); try testing.expectEqualDeep(HostPort{ .ip = "*", .port = "5353" }, splitHostPort("*:5353")); } test "port parses IPv4 and IPv6 remotes" { try testing.expectEqual(@as(?u16, 443), port("1.2.3.4:443")); try testing.expectEqual(@as(?u16, 443), port("[2a00:1450::5]:443")); try testing.expectEqual(@as(?u16, null), port("")); try testing.expectEqual(@as(?u16, null), port("1.2.3.4:")); } test "isPrivate" { for ([_][]const u8{ "10.1.2.3", "192.168.1.1", "172.16.0.1", "172.31.255.255", "127.0.0.1", "169.254.1.1", "::1", "fe80::1", "fd00::1", "fc00::1" }) |ip| { try testing.expect(isPrivate(ip)); } for ([_][]const u8{ "1.1.1.1", "172.32.0.1", "2606:4700:4700::1111", "not an address" }) |ip| { try testing.expect(!isPrivate(ip)); } }