master
  1const builtin = @import("builtin");
  2
  3const std = @import("std");
  4const Io = std.Io;
  5const net = std.Io.net;
  6const mem = std.mem;
  7const testing = std.testing;
  8
  9test "parse and render IP addresses at comptime" {
 10    comptime {
 11        const ipv6addr = net.IpAddress.parse("::1", 0) catch unreachable;
 12        try testing.expectFmt("[::1]:0", "{f}", .{ipv6addr});
 13
 14        const ipv4addr = net.IpAddress.parse("127.0.0.1", 0) catch unreachable;
 15        try testing.expectFmt("127.0.0.1:0", "{f}", .{ipv4addr});
 16
 17        try testing.expectError(error.ParseFailed, net.IpAddress.parse("::123.123.123.123", 0));
 18        try testing.expectError(error.ParseFailed, net.IpAddress.parse("127.01.0.1", 0));
 19    }
 20}
 21
 22test "format IPv6 address with no zero runs" {
 23    const addr = try net.IpAddress.parseIp6("2001:db8:1:2:3:4:5:6", 0);
 24    try testing.expectFmt("[2001:db8:1:2:3:4:5:6]:0", "{f}", .{addr});
 25}
 26
 27test "parse IPv6 addresses and check compressed form" {
 28    try testing.expectFmt("[2001:db8::1:0:0:2]:0", "{f}", .{
 29        try net.IpAddress.parseIp6("2001:0db8:0000:0000:0001:0000:0000:0002", 0),
 30    });
 31    try testing.expectFmt("[2001:db8::1:2]:0", "{f}", .{
 32        try net.IpAddress.parseIp6("2001:0db8:0000:0000:0000:0000:0001:0002", 0),
 33    });
 34    try testing.expectFmt("[2001:db8:1:0:1::2]:0", "{f}", .{
 35        try net.IpAddress.parseIp6("2001:0db8:0001:0000:0001:0000:0000:0002", 0),
 36    });
 37}
 38
 39test "parse IPv6 address, check raw bytes" {
 40    const expected_raw: [16]u8 = .{
 41        0x20, 0x01, 0x0d, 0xb8, // 2001:db8
 42        0x00, 0x00, 0x00, 0x00, // :0000:0000
 43        0x00, 0x01, 0x00, 0x00, // :0001:0000
 44        0x00, 0x00, 0x00, 0x02, // :0000:0002
 45    };
 46    const addr = try net.IpAddress.parseIp6("2001:db8:0000:0000:0001:0000:0000:0002", 0);
 47    try testing.expectEqualSlices(u8, &expected_raw, &addr.ip6.bytes);
 48}
 49
 50test "parse and render IPv6 addresses" {
 51    try testParseAndRenderIp6Address("FF01:0:0:0:0:0:0:FB", "ff01::fb");
 52    try testParseAndRenderIp6Address("FF01::Fb", "ff01::fb");
 53    try testParseAndRenderIp6Address("::1", "::1");
 54    try testParseAndRenderIp6Address("::", "::");
 55    try testParseAndRenderIp6Address("1::", "1::");
 56    try testParseAndRenderIp6Address("2001:db8::", "2001:db8::");
 57    try testParseAndRenderIp6Address("::1234:5678", "::1234:5678");
 58    try testParseAndRenderIp6Address("2001:db8::1234:5678", "2001:db8::1234:5678");
 59    try testParseAndRenderIp6Address("FF01::FB%1234", "ff01::fb%1234");
 60    try testParseAndRenderIp6Address("::ffff:123.5.123.5", "::ffff:123.5.123.5");
 61    try testParseAndRenderIp6Address("ff01::fb%12345678901234", "ff01::fb%12345678901234");
 62}
 63
 64fn testParseAndRenderIp6Address(input: []const u8, expected_output: []const u8) !void {
 65    var buffer: [100]u8 = undefined;
 66    const parsed = net.Ip6Address.Unresolved.parse(input);
 67    const actual_printed = try std.fmt.bufPrint(&buffer, "{f}", .{parsed.success});
 68    try testing.expectEqualStrings(expected_output, actual_printed);
 69}
 70
 71test "IPv6 address parse failures" {
 72    try testing.expectError(error.ParseFailed, net.IpAddress.parseIp6(":::", 0));
 73
 74    const Unresolved = net.Ip6Address.Unresolved;
 75
 76    try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 2 }, Unresolved.parse(":::"));
 77    try testing.expectEqual(Unresolved.Parsed{ .overflow = 4 }, Unresolved.parse("FF001::FB"));
 78    try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 9 }, Unresolved.parse("FF01::Fb:zig"));
 79    try testing.expectEqual(Unresolved.Parsed{ .junk_after_end = 19 }, Unresolved.parse("FF01:0:0:0:0:0:0:FB:"));
 80    try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("FF01:"));
 81    try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 5 }, Unresolved.parse("::123.123.123.123"));
 82    try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("1"));
 83    try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("ff01::fb%"));
 84}
 85
 86test "invalid but parseable IPv6 scope ids" {
 87    const io = testing.io;
 88
 89    if (builtin.os.tag != .linux and comptime !builtin.os.tag.isDarwin()) {
 90        return error.SkipZigTest; // TODO
 91    }
 92
 93    try testing.expectError(error.InterfaceNotFound, net.IpAddress.resolveIp6(io, "ff01::fb%123s45678901234", 0));
 94}
 95
 96test "parse and render IPv4 addresses" {
 97    var buffer: [18]u8 = undefined;
 98    for ([_][]const u8{
 99        "0.0.0.0",
100        "255.255.255.255",
101        "1.2.3.4",
102        "123.255.0.91",
103        "127.0.0.1",
104    }) |ip| {
105        const addr = net.IpAddress.parseIp4(ip, 0) catch unreachable;
106        var newIp = std.fmt.bufPrint(buffer[0..], "{f}", .{addr}) catch unreachable;
107        try testing.expect(std.mem.eql(u8, ip, newIp[0 .. newIp.len - 2]));
108    }
109
110    try testing.expectError(error.Overflow, net.IpAddress.parseIp4("256.0.0.1", 0));
111    try testing.expectError(error.InvalidCharacter, net.IpAddress.parseIp4("x.0.0.1", 0));
112    try testing.expectError(error.InvalidEnd, net.IpAddress.parseIp4("127.0.0.1.1", 0));
113    try testing.expectError(error.Incomplete, net.IpAddress.parseIp4("127.0.0.", 0));
114    try testing.expectError(error.InvalidCharacter, net.IpAddress.parseIp4("100..0.1", 0));
115    try testing.expectError(error.NonCanonical, net.IpAddress.parseIp4("127.01.0.1", 0));
116}
117
118test "resolve DNS" {
119    if (builtin.os.tag == .wasi) return error.SkipZigTest;
120
121    const io = testing.io;
122
123    // Resolve localhost, this should not fail.
124    {
125        const localhost_v4 = try net.IpAddress.parse("127.0.0.1", 80);
126        const localhost_v6 = try net.IpAddress.parse("::2", 80);
127
128        var canonical_name_buffer: [net.HostName.max_len]u8 = undefined;
129        var results_buffer: [32]net.HostName.LookupResult = undefined;
130        var results: Io.Queue(net.HostName.LookupResult) = .init(&results_buffer);
131
132        net.HostName.lookup(try .init("localhost"), io, &results, .{
133            .port = 80,
134            .canonical_name_buffer = &canonical_name_buffer,
135        });
136
137        var addresses_found: usize = 0;
138
139        while (results.getOne(io)) |result| switch (result) {
140            .address => |address| {
141                if (address.eql(&localhost_v4) or address.eql(&localhost_v6))
142                    addresses_found += 1;
143            },
144            .canonical_name => |canonical_name| try testing.expectEqualStrings("localhost", canonical_name.bytes),
145            .end => |end| {
146                try end;
147                break;
148            },
149        } else |err| return err;
150
151        try testing.expect(addresses_found != 0);
152    }
153
154    {
155        // The tests are required to work even when there is no Internet connection,
156        // so some of these errors we must accept and skip the test.
157        var canonical_name_buffer: [net.HostName.max_len]u8 = undefined;
158        var results_buffer: [16]net.HostName.LookupResult = undefined;
159        var results: Io.Queue(net.HostName.LookupResult) = .init(&results_buffer);
160
161        net.HostName.lookup(try .init("example.com"), io, &results, .{
162            .port = 80,
163            .canonical_name_buffer = &canonical_name_buffer,
164        });
165
166        while (results.getOne(io)) |result| switch (result) {
167            .address => {},
168            .canonical_name => {},
169            .end => |end| {
170                end catch |err| switch (err) {
171                    error.UnknownHostName => return error.SkipZigTest,
172                    error.NameServerFailure => return error.SkipZigTest,
173                    else => return err,
174                };
175                break;
176            },
177        } else |err| return err;
178    }
179}
180
181test "listen on a port, send bytes, receive bytes" {
182    if (builtin.single_threaded) return error.SkipZigTest;
183    if (builtin.os.tag == .wasi) return error.SkipZigTest;
184
185    const io = testing.io;
186
187    // Try only the IPv4 variant as some CI builders have no IPv6 localhost
188    // configured.
189    const localhost: net.IpAddress = .{ .ip4 = .loopback(0) };
190
191    var server = try localhost.listen(io, .{});
192    defer server.deinit(io);
193
194    const S = struct {
195        fn clientFn(server_address: net.IpAddress) !void {
196            var stream = try server_address.connect(io, .{ .mode = .stream });
197            defer stream.close(io);
198
199            var stream_writer = stream.writer(io, &.{});
200            try stream_writer.interface.writeAll("Hello world!");
201        }
202    };
203
204    const t = try std.Thread.spawn(.{}, S.clientFn, .{server.socket.address});
205    defer t.join();
206
207    var stream = try server.accept(io);
208    defer stream.close(io);
209    var buf: [16]u8 = undefined;
210    var stream_reader = stream.reader(io, &.{});
211    const n = try stream_reader.interface.readSliceShort(&buf);
212
213    try testing.expectEqual(@as(usize, 12), n);
214    try testing.expectEqualSlices(u8, "Hello world!", buf[0..n]);
215}
216
217test "listen on an in use port" {
218    if (builtin.os.tag != .linux and comptime !builtin.os.tag.isDarwin() and builtin.os.tag != .windows) {
219        // TODO build abstractions for other operating systems
220        return error.SkipZigTest;
221    }
222
223    const io = testing.io;
224
225    const localhost: net.IpAddress = .{ .ip4 = .loopback(0) };
226
227    var server1 = try localhost.listen(io, .{ .reuse_address = true });
228    defer server1.deinit(io);
229
230    var server2 = try server1.socket.address.listen(io, .{ .reuse_address = true });
231    defer server2.deinit(io);
232}
233
234fn testClientToHost(allocator: mem.Allocator, name: []const u8, port: u16) anyerror!void {
235    if (builtin.os.tag == .wasi) return error.SkipZigTest;
236
237    const connection = try net.tcpConnectToHost(allocator, name, port);
238    defer connection.close();
239
240    var buf: [100]u8 = undefined;
241    const len = try connection.read(&buf);
242    const msg = buf[0..len];
243    try testing.expect(mem.eql(u8, msg, "hello from server\n"));
244}
245
246fn testClient(addr: net.IpAddress) anyerror!void {
247    if (builtin.os.tag == .wasi) return error.SkipZigTest;
248
249    const socket_file = try net.tcpConnectToAddress(addr);
250    defer socket_file.close();
251
252    var buf: [100]u8 = undefined;
253    const len = try socket_file.read(&buf);
254    const msg = buf[0..len];
255    try testing.expect(mem.eql(u8, msg, "hello from server\n"));
256}
257
258fn testServer(server: *net.Server) anyerror!void {
259    if (builtin.os.tag == .wasi) return error.SkipZigTest;
260
261    const io = testing.io;
262
263    var stream = try server.accept(io);
264    var writer = stream.writer(io, &.{});
265    try writer.interface.print("hello from server\n", .{});
266}
267
268test "listen on a unix socket, send bytes, receive bytes" {
269    if (builtin.single_threaded) return error.SkipZigTest;
270    if (!net.has_unix_sockets) return error.SkipZigTest;
271    if (builtin.os.tag == .windows) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/25983
272
273    const io = testing.io;
274
275    const socket_path = try generateFileName("socket.unix");
276    defer testing.allocator.free(socket_path);
277
278    const socket_addr = try net.UnixAddress.init(socket_path);
279    defer std.fs.cwd().deleteFile(socket_path) catch {};
280
281    var server = try socket_addr.listen(io, .{});
282    defer server.socket.close(io);
283
284    const S = struct {
285        fn clientFn(path: []const u8) !void {
286            const server_path: net.UnixAddress = try .init(path);
287            var stream = try server_path.connect(io);
288            defer stream.close(io);
289
290            var stream_writer = stream.writer(io, &.{});
291            try stream_writer.interface.writeAll("Hello world!");
292        }
293    };
294
295    const t = try std.Thread.spawn(.{}, S.clientFn, .{socket_path});
296    defer t.join();
297
298    var stream = try server.accept(io);
299    defer stream.close(io);
300    var buf: [16]u8 = undefined;
301    var stream_reader = stream.reader(io, &.{});
302    const n = try stream_reader.interface.readSliceShort(&buf);
303
304    try testing.expectEqual(@as(usize, 12), n);
305    try testing.expectEqualSlices(u8, "Hello world!", buf[0..n]);
306}
307
308fn generateFileName(base_name: []const u8) ![]const u8 {
309    const random_bytes_count = 12;
310    const sub_path_len = comptime std.fs.base64_encoder.calcSize(random_bytes_count);
311    var random_bytes: [12]u8 = undefined;
312    std.crypto.random.bytes(&random_bytes);
313    var sub_path: [sub_path_len]u8 = undefined;
314    _ = std.fs.base64_encoder.encode(&sub_path, &random_bytes);
315    return std.fmt.allocPrint(testing.allocator, "{s}-{s}", .{ sub_path[0..], base_name });
316}
317
318test "non-blocking tcp server" {
319    if (builtin.os.tag == .wasi) return error.SkipZigTest;
320    if (true) {
321        // https://github.com/ziglang/zig/issues/18315
322        return error.SkipZigTest;
323    }
324
325    const io = testing.io;
326
327    const localhost: net.IpAddress = .{ .ip4 = .loopback(0) };
328    var server = localhost.listen(io, .{ .force_nonblocking = true });
329    defer server.deinit(io);
330
331    const accept_err = server.accept(io);
332    try testing.expectError(error.WouldBlock, accept_err);
333
334    const socket_file = try net.tcpConnectToAddress(server.socket.address);
335    defer socket_file.close();
336
337    var stream = try server.accept(io);
338    defer stream.close(io);
339    var writer = stream.writer(io, .{});
340    try writer.interface.print("hello from server\n", .{});
341
342    var buf: [100]u8 = undefined;
343    const len = try socket_file.read(&buf);
344    const msg = buf[0..len];
345    try testing.expect(mem.eql(u8, msg, "hello from server\n"));
346}
347
348test "decompress compressed DNS name" {
349    const packet = &[_]u8{
350        // Header section
351        // ----------------------------------------------------------
352        0x00, 0x00, // ID: 0
353        0x81, 0x80, // Flags (QR:1, RCODE:0, etc.)
354        0x00, 0x01, // QDCOUNT: 1 (Question Count)
355        0x00, 0x01, // ANCOUNT: 1 (Answer Count)
356        0x00, 0x00, // NSCOUNT: 0
357        0x00, 0x00, // ARCOUNT: 0
358        // ----------------------------------------------------------
359
360        // Question section (12 byte offset)
361        // ----------------------------------------------------------
362        // QNAME: "www.ziglang.org"
363        0x03, 'w', 'w', 'w', // label
364        0x07, 'z', 'i', 'g', 'l', 'a', 'n', 'g', // label
365        0x03, 'o', 'r', 'g', // label
366        0x00, // null label
367        0x00, 0x01, // QTYPE: A
368        0x00, 0x01, // QCLASS: IN
369        // ----------------------------------------------------------
370
371        // Resource Record (Answer) (33 byte offset)
372        // ----------------------------------------------------------
373        0xc0, 0x0c, // NAME: pointer to 0x0c ("www.ziglang.org")
374        0x00, 0x05, // TYPE: CNAME
375        0x00, 0x01, // CLASS: IN
376        0x00, 0x00, 0x01, 0x00, // TTL: 256 (seconds)
377        0x00, 0x06, // RDLENGTH: 6 bytes
378
379        // RDATA (45 byte offset): "target.ziglang.org" (compressed)
380        0x06, 't', 'a', 'r', 'g', 'e', 't', // 6, "target"
381        0xc0, 0x10, // pointer (0xc0 flag) to 0x10 byte offset ("ziglang.org")
382        // ----------------------------------------------------------
383    };
384
385    // The start index of the CNAME RDATA is 45
386    const cname_data_index = 45;
387    var dest_buffer: [net.HostName.max_len]u8 = undefined;
388
389    const n_consumed, const result = try net.HostName.expand(
390        packet,
391        cname_data_index,
392        &dest_buffer,
393    );
394    try testing.expectEqual(packet.len - cname_data_index, n_consumed);
395    try testing.expectEqualStrings("target.ziglang.org", result.bytes);
396}