lib/peer/src/address.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 /// Defines the package bound of 253 bytes for relay host text. Under [RFC1035
4 /// section 2.3.4](https://www.rfc-editor.org/rfc/rfc1035.html#section-2.3.4), a
5 /// DNS wire-format name is limited to 255 octets including label length octets
6 /// and the terminating zero. Standard unescaped dotted presentation text
7 /// without a trailing root dot can therefore occupy 253 bytes. A trailing dot
8 /// or escaped text alters presentation length, so the RFC does not define a
9 /// universal 253-byte text limit across all representations. Relay parsing
10 /// enforces this storage bound but does not fully validate DNS label syntax.
11 pub const relay_host_bytes_max: usize = 253;
12 pub const text_bytes_max: usize = "relay:".len + relay_host_bytes_max + 1 + 5;
13 pub const RelayInitError = error{
14 HostEmpty,
15 HostTooLong,
16 InvalidHost,
17 };
18 pub const AddressBaseParseError = error{
19 InvalidAddress,
20 InvalidPort,
21 };
22 pub const AddressParseError = RelayInitError || AddressBaseParseError;
23
24 pub const Ip4 = struct {
25 addr: [4]u8,
26 port: u16,
27 };
28
29 pub const Ip6 = struct {
30 addr: [16]u8,
31 port: u16,
32 };
33
34 pub const Relay = struct {
35 bytes: [relay_host_bytes_max]u8,
36 len: u8,
37 port: u16,
38
39 pub const InitError: type = RelayInitError;
40
41 pub fn init(host_name: []const u8, port: u16) InitError!Relay {
42 if (host_name.len == 0) return error.HostEmpty;
43 if (host_name.len > relay_host_bytes_max) return error.HostTooLong;
44 for (host_name) |byte| {
45 if (!hostByteValid(byte)) return error.InvalidHost;
46 }
47 var result = Relay{
48 .bytes = @splat(0),
49 .len = @intCast(host_name.len),
50 .port = port,
51 };
52 @memcpy(result.bytes[0..host_name.len], host_name);
53 return result;
54 }
55
56 pub fn host(self: *const Relay) []const u8 {
57 std.debug.assert(self.len <= relay_host_bytes_max);
58 return self.bytes[0..self.len];
59 }
60
61 pub fn valid(self: *const Relay) bool {
62 if (self.len == 0 or self.len > relay_host_bytes_max) return false;
63 for (self.bytes[0..self.len]) |byte| {
64 if (!hostByteValid(byte)) return false;
65 }
66 return true;
67 }
68
69 pub fn eql(self: *const Relay, other: *const Relay) bool {
70 return self.port == other.port and std.mem.eql(u8, self.host(), other.host());
71 }
72 };
73
74 pub const Address = union(enum) {
75 ip4: Ip4,
76 ip6: Ip6,
77 relay: Relay,
78
79 pub const FormatError: type = error{OutputTooSmall};
80 pub const ParseError: type = AddressParseError;
81
82 pub fn fromRelay(host_name: []const u8, port: u16) Relay.InitError!Address {
83 return .{ .relay = try Relay.init(host_name, port) };
84 }
85
86 pub fn format(self: Address, output: []u8) FormatError![]const u8 {
87 var writer = std.Io.Writer.fixed(output);
88 switch (self) {
89 .ip4 => |value| writer.print(
90 "{d}.{d}.{d}.{d}:{d}",
91 .{ value.addr[0], value.addr[1], value.addr[2], value.addr[3], value.port },
92 ) catch return error.OutputTooSmall,
93 .ip6 => |value| {
94 const address = std.Io.net.Ip6Address{
95 .bytes = value.addr,
96 .port = value.port,
97 };
98 address.format(&writer) catch return error.OutputTooSmall;
99 },
100 .relay => |value| writer.print(
101 "relay:{s}:{d}",
102 .{ value.host(), value.port },
103 ) catch return error.OutputTooSmall,
104 }
105 return writer.buffered();
106 }
107
108 pub fn parse(source: []const u8) ParseError!Address {
109 if (std.mem.startsWith(u8, source, "relay:")) return parseRelay(source);
110 if (source.len != 0 and source[0] == '[') return parseIp6(source);
111 return parseIp4(source);
112 }
113
114 pub fn eql(self: Address, other: Address) bool {
115 if (std.meta.activeTag(self) != std.meta.activeTag(other)) return false;
116 return switch (self) {
117 .ip4 => |left| switch (other) {
118 .ip4 => |right| left.port == right.port and
119 std.mem.eql(u8, &left.addr, &right.addr),
120 else => unreachable,
121 },
122 .ip6 => |left| switch (other) {
123 .ip6 => |right| left.port == right.port and
124 std.mem.eql(u8, &left.addr, &right.addr),
125 else => unreachable,
126 },
127 .relay => |left| switch (other) {
128 .relay => |right| left.eql(&right),
129 else => unreachable,
130 },
131 };
132 }
133 };
134
135 fn parseRelay(source: []const u8) Address.ParseError!Address {
136 const payload = source["relay:".len..];
137 const separator = std.mem.lastIndexOfScalar(u8, payload, ':') orelse
138 return error.InvalidAddress;
139 const host_name = payload[0..separator];
140 const port_text = payload[separator + 1 ..];
141 return Address.fromRelay(host_name, try parsePort(port_text));
142 }
143
144 fn parseIp4(source: []const u8) Address.ParseError!Address {
145 const separator = std.mem.lastIndexOfScalar(u8, source, ':') orelse
146 return error.InvalidAddress;
147 const host = source[0..separator];
148 const port_text = source[separator + 1 ..];
149 const parsed = std.Io.net.Ip4Address.parse(host, try parsePort(port_text)) catch
150 return error.InvalidAddress;
151 return .{ .ip4 = .{ .addr = parsed.bytes, .port = parsed.port } };
152 }
153
154 fn parseIp6(source: []const u8) Address.ParseError!Address {
155 const close = std.mem.indexOfScalar(u8, source, ']') orelse
156 return error.InvalidAddress;
157 if (close == 0) return error.InvalidAddress;
158 const suffix = source[close + 1 ..];
159 if (suffix.len < 2 or suffix[0] != ':') return error.InvalidAddress;
160 const parsed = std.Io.net.Ip6Address.parse(
161 source[1..close],
162 try parsePort(suffix[1..]),
163 ) catch return error.InvalidAddress;
164 return .{ .ip6 = .{ .addr = parsed.bytes, .port = parsed.port } };
165 }
166
167 fn parsePort(source: []const u8) error{InvalidPort}!u16 {
168 if (source.len == 0) return error.InvalidPort;
169 if (source.len > 1 and source[0] == '0') return error.InvalidPort;
170 return std.fmt.parseUnsigned(u16, source, 10) catch error.InvalidPort;
171 }
172
173 fn hostByteValid(byte: u8) bool {
174 return std.ascii.isAlphabetic(byte) or std.ascii.isDigit(byte) or
175 byte == '-' or byte == '.';
176 }
177
178 test "IPv4 address formats and parses" {
179 const expected = Address{ .ip4 = .{ .addr = .{ 1, 2, 3, 4 }, .port = 443 } };
180 var output: [text_bytes_max]u8 = undefined;
181 const formatted = try expected.format(&output);
182 try std.testing.expectEqualStrings("1.2.3.4:443", formatted);
183 try std.testing.expect(expected.eql(try Address.parse(formatted)));
184 }
185
186 test "IPv6 address formats and parses" {
187 const expected = Address{ .ip6 = .{
188 .addr = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
189 .port = 443,
190 } };
191 var output: [text_bytes_max]u8 = undefined;
192 const formatted = try expected.format(&output);
193 try std.testing.expectEqualStrings("[::1]:443", formatted);
194 try std.testing.expect(expected.eql(try Address.parse(formatted)));
195 }
196
197 test "relay address formats and parses" {
198 const expected = try Address.fromRelay("Relay-1.example", 8443);
199 var output: [text_bytes_max]u8 = undefined;
200 const formatted = try expected.format(&output);
201 try std.testing.expectEqualStrings("relay:Relay-1.example:8443", formatted);
202 try std.testing.expect(expected.eql(try Address.parse(formatted)));
203 }
204
205 test "relay host accepts 253 bytes and rejects max plus one" {
206 const maximum: [relay_host_bytes_max]u8 = @splat('a');
207 const accepted = try Address.fromRelay(&maximum, 1);
208 try std.testing.expectEqual(relay_host_bytes_max, accepted.relay.host().len);
209 const oversized: [relay_host_bytes_max + 1]u8 = @splat('a');
210 try std.testing.expectError(error.HostTooLong, Address.fromRelay(&oversized, 1));
211 try std.testing.expectError(error.InvalidHost, Address.fromRelay("bad_host", 1));
212 }