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 }