| 1 | //! An already-validated host name. A valid host name: |
| 2 | //! * Has length less than or equal to `max_len`. |
| 3 | //! * Is valid UTF-8. |
| 4 | //! * Lacks ASCII characters other than alphanumeric, '-', and '.'. |
| 5 | const HostName = @This(); |
| 6 | |
| 7 | const builtin = @import("builtin"); |
| 8 | const native_os = builtin.os.tag; |
| 9 | |
| 10 | const std = @import("../../std.zig"); |
| 11 | const Io = std.Io; |
| 12 | const IpAddress = Io.net.IpAddress; |
| 13 | const Ip6Address = Io.net.Ip6Address; |
| 14 | const assert = std.debug.assert; |
| 15 | const Stream = Io.net.Stream; |
| 16 | |
| 17 | /// Externally managed memory. Already checked to be valid. |
| 18 | bytes: []const u8, |
| 19 | |
| 20 | /// The maximum number of bytes needed to store the text representation of |
| 21 | /// a max length host name, where labels are separated by dots, including |
| 22 | /// the trailing dot and the zero-length root label. |
| 23 | /// |
| 24 | /// The max length of a host name is determined by its packet representation, |
| 25 | /// where each label has a length prefix of 1 octet, the root label has a |
| 26 | /// length of 0, and the maximum total number of octets is 255. |
| 27 | /// |
| 28 | /// See [RFC 1035, Section 3.1](https://datatracker.ietf.org/doc/html/rfc1035#section-3.1) |
| 29 | pub const max_len = 254; |
| 30 | const max_len_without_root = max_len - 1; |
| 31 | |
| 32 | pub const FromUriError = error{UriMissingHost} || ValidateError; |
| 33 | |
| 34 | /// Returned `HostName.bytes` may point into `buffer` or `uri.host`. |
| 35 | pub fn fromUri(uri: std.Uri, buffer: *[HostName.max_len]u8) FromUriError!HostName { |
| 36 | const component = uri.host orelse return error.UriMissingHost; |
| 37 | const bytes = component.toRaw(buffer) catch |err| switch (err) { |
| 38 | error.NoSpaceLeft => return error.NameTooLong, |
| 39 | }; |
| 40 | try validate(bytes); |
| 41 | return .{ .bytes = bytes }; |
| 42 | } |
| 43 | |
| 44 | pub const ValidateError = error{ |
| 45 | NameTooLong, |
| 46 | InvalidHostName, |
| 47 | }; |
| 48 | |
| 49 | /// Validates a hostname according to [RFC 1123](https://www.rfc-editor.org/rfc/rfc1123) |
| 50 | pub fn validate(bytes: []const u8) ValidateError!void { |
| 51 | if (bytes.len == 0) return error.InvalidHostName; |
| 52 | |
| 53 | // Ignore trailing dot (FQDN). |
| 54 | const end = if (bytes[bytes.len - 1] == '.') bytes.len - 1 else bytes.len; |
| 55 | |
| 56 | // The accepted maximum length of a hostname, including labels and dots. |
| 57 | if (end > max_len_without_root) return error.NameTooLong; |
| 58 | |
| 59 | // Hostnames are divided into dot-separated "labels", which: |
| 60 | // |
| 61 | // - Start with a letter or digit |
| 62 | // - Can contain letters, digits, or hyphens |
| 63 | // - Must end with a letter or digit |
| 64 | // - Have a minimum of 1 character and a maximum of 63 |
| 65 | var label_len: usize = 0; |
| 66 | for (bytes[0..end], 0..) |c, i| { |
| 67 | switch (c) { |
| 68 | '.' => { |
| 69 | if (label_len == 0 or label_len > 63) return error.InvalidHostName; |
| 70 | if (!std.ascii.isAlphanumeric(bytes[i - 1])) return error.InvalidHostName; |
| 71 | label_len = 0; |
| 72 | }, |
| 73 | '-' => { |
| 74 | if (label_len == 0) return error.InvalidHostName; |
| 75 | label_len += 1; |
| 76 | }, |
| 77 | else => { |
| 78 | if (!std.ascii.isAlphanumeric(c)) return error.InvalidHostName; |
| 79 | label_len += 1; |
| 80 | }, |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | // Validate the final label |
| 85 | if (label_len == 0 or label_len > 63) return error.InvalidHostName; |
| 86 | if (!std.ascii.isAlphanumeric(bytes[end - 1])) return error.InvalidHostName; |
| 87 | } |
| 88 | |
| 89 | test validate { |
| 90 | // Valid hostnames |
| 91 | try validate("example"); |
| 92 | try validate("example.com"); |
| 93 | try validate("www.example.com"); |
| 94 | try validate("sub.domain.example.com"); |
| 95 | try validate("example.com."); |
| 96 | try validate("host-name.example.com."); |
| 97 | try validate("123.example.com."); |
| 98 | try validate("a-b.com"); |
| 99 | try validate("a.b.c.d.e.f.g"); |
| 100 | try validate("127.0.0.1"); // Also a valid hostname |
| 101 | |
| 102 | const many_a: [63]u8 = @splat('a'); |
| 103 | try validate(&many_a ++ ".com"); // Label exactly 63 chars (valid) |
| 104 | |
| 105 | const many_a_dot_buf: [126][2]u8 = @splat(.{ 'a', '.' }); |
| 106 | const many_a_dot: []const u8 = @ptrCast(&many_a_dot_buf); |
| 107 | try validate(many_a_dot ++ "a"); // Total length 253 (without the trailing dot) |
| 108 | try validate(many_a_dot ++ "a."); // Total length 254 (with the trailing dot) |
| 109 | |
| 110 | // Invalid hostnames |
| 111 | try std.testing.expectError(error.InvalidHostName, validate("")); |
| 112 | try std.testing.expectError(error.InvalidHostName, validate(".example.com")); |
| 113 | try std.testing.expectError(error.InvalidHostName, validate("example.com..")); |
| 114 | try std.testing.expectError(error.InvalidHostName, validate("host..domain")); |
| 115 | try std.testing.expectError(error.InvalidHostName, validate("-hostname")); |
| 116 | try std.testing.expectError(error.InvalidHostName, validate("hostname-")); |
| 117 | try std.testing.expectError(error.InvalidHostName, validate("hostname-.com")); |
| 118 | try std.testing.expectError(error.InvalidHostName, validate("a.-.b")); |
| 119 | try std.testing.expectError(error.InvalidHostName, validate("host_name.com")); |
| 120 | try std.testing.expectError(error.InvalidHostName, validate(".")); |
| 121 | try std.testing.expectError(error.InvalidHostName, validate("..")); |
| 122 | try std.testing.expectError(error.InvalidHostName, validate(&many_a ++ "a.com")); // Label length 64 (too long) |
| 123 | try std.testing.expectError(error.NameTooLong, validate(many_a_dot ++ "ab")); // Total length 254 (without the trailing dot) |
| 124 | try std.testing.expectError(error.NameTooLong, validate(many_a_dot ++ "ab.")); // Total length 255 (with the trailing dot) |
| 125 | } |
| 126 | |
| 127 | pub fn init(bytes: []const u8) ValidateError!HostName { |
| 128 | try validate(bytes); |
| 129 | return .{ .bytes = bytes }; |
| 130 | } |
| 131 | |
| 132 | pub fn sameParentDomain(parent_host: HostName, child_host: HostName) bool { |
| 133 | const parent_bytes = parent_host.bytes; |
| 134 | const child_bytes = child_host.bytes; |
| 135 | if (!std.ascii.endsWithIgnoreCase(child_bytes, parent_bytes)) return false; |
| 136 | if (child_bytes.len == parent_bytes.len) return true; |
| 137 | if (parent_bytes.len > child_bytes.len) return false; |
| 138 | return child_bytes[child_bytes.len - parent_bytes.len - 1] == '.'; |
| 139 | } |
| 140 | |
| 141 | test sameParentDomain { |
| 142 | try std.testing.expect(!sameParentDomain(try .init("foo.com"), try .init("bar.com"))); |
| 143 | try std.testing.expect(sameParentDomain(try .init("foo.com"), try .init("foo.com"))); |
| 144 | try std.testing.expect(sameParentDomain(try .init("foo.com"), try .init("bar.foo.com"))); |
| 145 | try std.testing.expect(!sameParentDomain(try .init("bar.foo.com"), try .init("foo.com"))); |
| 146 | } |
| 147 | |
| 148 | /// Domain names are case-insensitive (RFC 5890, Section 2.3.2.4) |
| 149 | pub fn eql(a: HostName, b: HostName) bool { |
| 150 | return std.ascii.eqlIgnoreCase(a.bytes, b.bytes); |
| 151 | } |
| 152 | |
| 153 | pub const LookupOptions = struct { |
| 154 | port: u16, |
| 155 | /// `null` means either. |
| 156 | canonical_name_buffer: ?*[max_len]u8 = null, |
| 157 | family: ?IpAddress.Family = null, |
| 158 | }; |
| 159 | |
| 160 | pub const LookupError = error{ |
| 161 | UnknownHostName, |
| 162 | ResolvConfParseFailed, |
| 163 | InvalidDnsARecord, |
| 164 | InvalidDnsAAAARecord, |
| 165 | InvalidDnsCnameRecord, |
| 166 | NameServerFailure, |
| 167 | NoAddressReturned, |
| 168 | /// Failed to open or read "/etc/hosts" or "/etc/resolv.conf". |
| 169 | DetectingNetworkConfigurationFailed, |
| 170 | } || IpAddress.BindError || Io.Cancelable; |
| 171 | |
| 172 | pub const LookupResult = union(enum) { |
| 173 | address: IpAddress, |
| 174 | canonical_name: HostName, |
| 175 | }; |
| 176 | |
| 177 | /// Adds any number of `LookupResult.address` into `resolved`, and exactly one |
| 178 | /// `LookupResult.canonical_name`. |
| 179 | /// |
| 180 | /// Guaranteed not to block if provided queue has capacity at least 16. |
| 181 | /// |
| 182 | /// Closes `resolved` before return, even on error. |
| 183 | /// |
| 184 | /// Asserts `resolved` is not closed until this call returns. |
| 185 | pub fn lookup( |
| 186 | host_name: HostName, |
| 187 | io: Io, |
| 188 | resolved: *Io.Queue(LookupResult), |
| 189 | options: LookupOptions, |
| 190 | ) LookupError!void { |
| 191 | return io.vtable.netLookup(io.userdata, host_name, resolved, options); |
| 192 | } |
| 193 | |
| 194 | pub const ExpandError = error{InvalidDnsPacket} || ValidateError; |
| 195 | |
| 196 | /// Decompresses a DNS name. |
| 197 | /// |
| 198 | /// Returns number of bytes consumed from `packet` starting at `i`, |
| 199 | /// along with the expanded `HostName`. |
| 200 | /// |
| 201 | /// Asserts `buffer` is has length at least `max_len`. |
| 202 | pub fn expand(noalias packet: []const u8, start_i: usize, noalias dest_buffer: []u8) ExpandError!struct { usize, HostName } { |
| 203 | const dest = dest_buffer[0..max_len]; |
| 204 | |
| 205 | var i = start_i; |
| 206 | var dest_i: usize = 0; |
| 207 | var len: ?usize = null; |
| 208 | |
| 209 | // Detect reference loop using an iteration counter. |
| 210 | for (0..packet.len / 2) |_| { |
| 211 | if (i >= packet.len) return error.InvalidDnsPacket; |
| 212 | |
| 213 | const c = packet[i]; |
| 214 | if ((c & 0xc0) != 0) { |
| 215 | if (i + 1 >= packet.len) return error.InvalidDnsPacket; |
| 216 | const j: usize = (@as(usize, c & 0x3F) << 8) | packet[i + 1]; |
| 217 | if (j >= packet.len) return error.InvalidDnsPacket; |
| 218 | if (len == null) len = (i + 2) - start_i; |
| 219 | i = j; |
| 220 | } else if (c != 0) { |
| 221 | if (dest_i != 0) { |
| 222 | dest[dest_i] = '.'; |
| 223 | dest_i += 1; |
| 224 | } |
| 225 | const label_len: usize = c; |
| 226 | if (i + 1 + label_len > packet.len) return error.InvalidDnsPacket; |
| 227 | if (dest_i + label_len + 1 > dest.len) return error.InvalidDnsPacket; |
| 228 | @memcpy(dest[dest_i..][0..label_len], packet[i + 1 ..][0..label_len]); |
| 229 | dest_i += label_len; |
| 230 | i += 1 + label_len; |
| 231 | } else { |
| 232 | return .{ |
| 233 | len orelse i - start_i + 1, |
| 234 | try .init(dest[0..dest_i]), |
| 235 | }; |
| 236 | } |
| 237 | } |
| 238 | return error.InvalidDnsPacket; |
| 239 | } |
| 240 | |
| 241 | pub const DnsRecord = enum(u8) { |
| 242 | A = 1, |
| 243 | CNAME = 5, |
| 244 | AAAA = 28, |
| 245 | _, |
| 246 | }; |
| 247 | |
| 248 | pub const DnsResponse = struct { |
| 249 | bytes: []const u8, |
| 250 | bytes_index: u32, |
| 251 | answers_remaining: u16, |
| 252 | |
| 253 | pub const Answer = struct { |
| 254 | rr: DnsRecord, |
| 255 | packet: []const u8, |
| 256 | data_off: u32, |
| 257 | data_len: u16, |
| 258 | }; |
| 259 | |
| 260 | pub const Error = error{InvalidDnsPacket}; |
| 261 | |
| 262 | pub fn init(r: []const u8) Error!DnsResponse { |
| 263 | if (r.len < 12) return error.InvalidDnsPacket; |
| 264 | if ((r[3] & 15) != 0) return .{ .bytes = r, .bytes_index = 3, .answers_remaining = 0 }; |
| 265 | var i: u32 = 12; |
| 266 | var query_count = std.mem.readInt(u16, r[4..6], .big); |
| 267 | while (query_count != 0) : (query_count -= 1) { |
| 268 | while (i < r.len and r[i] -% 1 < 127) i += 1; |
| 269 | if (r.len - i < 6) return error.InvalidDnsPacket; |
| 270 | i = i + 5 + @intFromBool(r[i] != 0); |
| 271 | } |
| 272 | return .{ |
| 273 | .bytes = r, |
| 274 | .bytes_index = i, |
| 275 | .answers_remaining = std.mem.readInt(u16, r[6..8], .big), |
| 276 | }; |
| 277 | } |
| 278 | |
| 279 | pub fn next(dr: *DnsResponse) Error!?Answer { |
| 280 | if (dr.answers_remaining == 0) return null; |
| 281 | dr.answers_remaining -= 1; |
| 282 | const r = dr.bytes; |
| 283 | var i = dr.bytes_index; |
| 284 | while (i < r.len and r[i] -% 1 < 127) i += 1; |
| 285 | if (r.len - i < 12) return error.InvalidDnsPacket; |
| 286 | i = i + 1 + @intFromBool(r[i] != 0); |
| 287 | const len = std.mem.readInt(u16, r[i + 8 ..][0..2], .big); |
| 288 | if (i + 10 + len > r.len) return error.InvalidDnsPacket; |
| 289 | defer dr.bytes_index = i + 10 + len; |
| 290 | return .{ |
| 291 | .rr = @fromBackingInt(@intCast(r[i + 1])), |
| 292 | .packet = r, |
| 293 | .data_off = i + 10, |
| 294 | .data_len = len, |
| 295 | }; |
| 296 | } |
| 297 | }; |
| 298 | |
| 299 | pub const ConnectError = LookupError || IpAddress.ConnectError; |
| 300 | |
| 301 | pub fn connect( |
| 302 | host_name: HostName, |
| 303 | io: Io, |
| 304 | port: u16, |
| 305 | options: IpAddress.ConnectOptions, |
| 306 | ) ConnectError!Stream { |
| 307 | var connect_many_buffer: [32]IpAddress.ConnectError!Stream = undefined; |
| 308 | var connect_many_queue: Io.Queue(IpAddress.ConnectError!Stream) = .init(&connect_many_buffer); |
| 309 | |
| 310 | var connect_many = io.async(connectMany, .{ host_name, io, port, &connect_many_queue, options }); |
| 311 | defer { |
| 312 | connect_many.cancel(io) catch {}; |
| 313 | while (connect_many_queue.getOneUncancelable(io)) |loser| { |
| 314 | if (loser) |s| s.close(io) else |_| {} |
| 315 | } else |err| switch (err) { |
| 316 | error.Closed => {}, |
| 317 | } |
| 318 | } |
| 319 | |
| 320 | var ip_connect_error: ?IpAddress.ConnectError = null; |
| 321 | |
| 322 | while (connect_many_queue.getOne(io)) |result| { |
| 323 | if (result) |stream| { |
| 324 | return stream; |
| 325 | } else |err| switch (err) { |
| 326 | error.Canceled => unreachable, |
| 327 | |
| 328 | error.SystemResources, |
| 329 | error.OptionUnsupported, |
| 330 | error.ProcessFdQuotaExceeded, |
| 331 | error.SystemFdQuotaExceeded, |
| 332 | => |e| return e, |
| 333 | |
| 334 | error.WouldBlock => return error.Unexpected, |
| 335 | |
| 336 | else => |e| ip_connect_error = e, |
| 337 | } |
| 338 | } else |err| switch (err) { |
| 339 | error.Canceled => |e| return e, |
| 340 | error.Closed => { |
| 341 | // There was no successful connection attempt. If there was a lookup error, return that. |
| 342 | try connect_many.await(io); |
| 343 | // Otherwise, return the error from a failed IP connection attempt. |
| 344 | return ip_connect_error orelse |
| 345 | return error.UnknownHostName; |
| 346 | }, |
| 347 | } |
| 348 | } |
| 349 | |
| 350 | /// Asynchronously establishes a connection to all IP addresses associated with |
| 351 | /// a host name, adding them to a results queue upon completion. |
| 352 | /// |
| 353 | /// `error.Canceled` will never be added to the queue, but other errors may be. |
| 354 | /// |
| 355 | /// Closes `results` before return, even on error. |
| 356 | /// |
| 357 | /// Asserts `results` is not closed until this call returns. |
| 358 | pub fn connectMany( |
| 359 | host_name: HostName, |
| 360 | io: Io, |
| 361 | port: u16, |
| 362 | results: *Io.Queue(IpAddress.ConnectError!Stream), |
| 363 | options: IpAddress.ConnectOptions, |
| 364 | ) LookupError!void { |
| 365 | defer results.close(io); |
| 366 | |
| 367 | var canonical_name_buffer: [max_len]u8 = undefined; |
| 368 | var lookup_buffer: [32]HostName.LookupResult = undefined; |
| 369 | var lookup_queue: Io.Queue(LookupResult) = .init(&lookup_buffer); |
| 370 | var lookup_future = io.async(lookup, .{ host_name, io, &lookup_queue, .{ |
| 371 | .port = port, |
| 372 | .canonical_name_buffer = &canonical_name_buffer, |
| 373 | } }); |
| 374 | defer lookup_future.cancel(io) catch {}; |
| 375 | |
| 376 | var group: Io.Group = .init; |
| 377 | defer group.cancel(io); |
| 378 | |
| 379 | while (lookup_queue.getOne(io)) |dns_result| switch (dns_result) { |
| 380 | .address => |address| group.async(io, enqueueConnection, .{ address, io, results, options }), |
| 381 | .canonical_name => continue, |
| 382 | } else |err| switch (err) { |
| 383 | error.Canceled => |e| return e, |
| 384 | error.Closed => { |
| 385 | try group.await(io); |
| 386 | return lookup_future.await(io); |
| 387 | }, |
| 388 | } |
| 389 | } |
| 390 | fn enqueueConnection( |
| 391 | address: IpAddress, |
| 392 | io: Io, |
| 393 | queue: *Io.Queue(IpAddress.ConnectError!Stream), |
| 394 | options: IpAddress.ConnectOptions, |
| 395 | ) Io.Cancelable!void { |
| 396 | const result = address.connect(io, options) catch |err| switch (err) { |
| 397 | error.Canceled => |e| return e, |
| 398 | else => |e| e, // other errors go in the result queue |
| 399 | }; |
| 400 | errdefer if (result) |s| s.close(io) else |_| {}; |
| 401 | queue.putOne(io, result) catch |err| switch (err) { |
| 402 | error.Canceled => |e| return e, |
| 403 | error.Closed => unreachable, // `queue` must not be closed |
| 404 | }; |
| 405 | } |
| 406 | |
| 407 | pub const ResolvConf = struct { |
| 408 | attempts: u32, |
| 409 | ndots: u32, |
| 410 | timeout_seconds: u32, |
| 411 | nameservers_buffer: [max_nameservers]IpAddress, |
| 412 | nameservers_len: usize, |
| 413 | search_buffer: [max_len]u8, |
| 414 | search_len: usize, |
| 415 | |
| 416 | /// According to resolv.conf(5) there is a maximum of 3 nameservers in this |
| 417 | /// file. |
| 418 | pub const max_nameservers = 3; |
| 419 | |
| 420 | /// Returns `error.StreamTooLong` if a line is longer than 512 bytes. |
| 421 | pub fn init(io: Io) !ResolvConf { |
| 422 | var rc: ResolvConf = .{ |
| 423 | .nameservers_buffer = undefined, |
| 424 | .nameservers_len = 0, |
| 425 | .search_buffer = undefined, |
| 426 | .search_len = 0, |
| 427 | .ndots = 1, |
| 428 | .timeout_seconds = 5, |
| 429 | .attempts = 2, |
| 430 | }; |
| 431 | |
| 432 | const file = Io.Dir.openFileAbsolute(io, "/etc/resolv.conf", .{}) catch |err| switch (err) { |
| 433 | error.FileNotFound, |
| 434 | error.NotDir, |
| 435 | error.AccessDenied, |
| 436 | => { |
| 437 | try addNumeric(&rc, io, "127.0.0.1", 53); |
| 438 | return rc; |
| 439 | }, |
| 440 | |
| 441 | else => |e| return e, |
| 442 | }; |
| 443 | defer file.close(io); |
| 444 | |
| 445 | var line_buf: [512]u8 = undefined; |
| 446 | var file_reader = file.reader(io, &line_buf); |
| 447 | parse(&rc, io, &file_reader.interface) catch |err| switch (err) { |
| 448 | error.ReadFailed => return file_reader.err.?, |
| 449 | else => |e| return e, |
| 450 | }; |
| 451 | return rc; |
| 452 | } |
| 453 | |
| 454 | const Directive = enum { options, nameserver, domain, search }; |
| 455 | const Option = enum { ndots, attempts, timeout }; |
| 456 | |
| 457 | pub fn parse(rc: *ResolvConf, io: Io, reader: *Io.Reader) !void { |
| 458 | while (reader.takeSentinel('\n')) |line_with_comment| { |
| 459 | const line = line: { |
| 460 | var split = std.mem.splitScalar(u8, line_with_comment, '#'); |
| 461 | break :line split.first(); |
| 462 | }; |
| 463 | var line_it = std.mem.tokenizeAny(u8, line, " \t"); |
| 464 | |
| 465 | const token = line_it.next() orelse continue; |
| 466 | switch (std.meta.stringToEnum(Directive, token) orelse continue) { |
| 467 | .options => while (line_it.next()) |sub_tok| { |
| 468 | var colon_it = std.mem.splitScalar(u8, sub_tok, ':'); |
| 469 | const name = colon_it.first(); |
| 470 | const value_txt = colon_it.next() orelse continue; |
| 471 | const value = std.fmt.parseInt(u8, value_txt, 10) catch |err| switch (err) { |
| 472 | error.Overflow => 255, |
| 473 | error.InvalidCharacter => continue, |
| 474 | }; |
| 475 | switch (std.meta.stringToEnum(Option, name) orelse continue) { |
| 476 | .ndots => rc.ndots = @min(value, 15), |
| 477 | .attempts => rc.attempts = @min(value, 10), |
| 478 | .timeout => rc.timeout_seconds = @min(value, 60), |
| 479 | } |
| 480 | }, |
| 481 | .nameserver => { |
| 482 | const ip_txt = line_it.next() orelse continue; |
| 483 | try addNumeric(rc, io, ip_txt, 53); |
| 484 | }, |
| 485 | .domain, .search => { |
| 486 | const rest = line_it.rest(); |
| 487 | @memcpy(rc.search_buffer[0..rest.len], rest); |
| 488 | rc.search_len = rest.len; |
| 489 | }, |
| 490 | } |
| 491 | } else |err| switch (err) { |
| 492 | error.EndOfStream => if (reader.bufferedLen() != 0) return error.EndOfStream, |
| 493 | else => |e| return e, |
| 494 | } |
| 495 | |
| 496 | if (rc.nameservers_len == 0) { |
| 497 | try addNumeric(rc, io, "127.0.0.1", 53); |
| 498 | } |
| 499 | } |
| 500 | |
| 501 | fn addNumeric(rc: *ResolvConf, io: Io, name: []const u8, port: u16) !void { |
| 502 | if (rc.nameservers_len < rc.nameservers_buffer.len) { |
| 503 | rc.nameservers_buffer[rc.nameservers_len] = try .resolve(io, name, port); |
| 504 | rc.nameservers_len += 1; |
| 505 | } |
| 506 | } |
| 507 | |
| 508 | pub fn nameservers(rc: *const ResolvConf) []const IpAddress { |
| 509 | return rc.nameservers_buffer[0..rc.nameservers_len]; |
| 510 | } |
| 511 | }; |
| 512 | |
| 513 | test ResolvConf { |
| 514 | const input = |
| 515 | \\# Generated by resolvconf |
| 516 | \\nameserver 1.0.0.1 |
| 517 | \\nameserver 1.1.1.1 |
| 518 | \\nameserver fe80::e0e:76ff:fed4:cf22 |
| 519 | \\options edns0 |
| 520 | \\ |
| 521 | ; |
| 522 | var reader: Io.Reader = .fixed(input); |
| 523 | |
| 524 | var rc: ResolvConf = .{ |
| 525 | .nameservers_buffer = undefined, |
| 526 | .nameservers_len = 0, |
| 527 | .search_buffer = undefined, |
| 528 | .search_len = 0, |
| 529 | .ndots = 1, |
| 530 | .timeout_seconds = 5, |
| 531 | .attempts = 2, |
| 532 | }; |
| 533 | |
| 534 | try rc.parse(std.testing.io, &reader); |
| 535 | try std.testing.expectEqual(3, rc.nameservers().len); |
| 536 | } |