authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-09-22 17:25:03-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-10-02 16:30:59-07:00
log0806252a9ba7e3bd1d3e9ed3af3d4579b1e675d9
tree7b4e099e57d597e7a7d9b184d02360e233f6932d
parent888bf35ab0bd6a248c2bcccd0dc670b33c328641

Io.net: finish implementing IPv6 parsing


3 files changed, 116 insertions(+), 30 deletions(-)

lib/std/Io/net.zig+112-26
......@@ -30,8 +30,8 @@ pub const IpAddress = union(enum) {
3030 /// This is a pure function but it cannot handle IPv6 addresses that have
3131 /// scope ids ("%foo" at the end). To also handle those, `resolve` must be
3232 /// called instead.
33 pub fn parse(name: []const u8, port: u16) !IpAddress {
34 if (parseIp4(name, port)) |ip4| return ip4 else |err| switch (err) {
33 pub fn parse(text: []const u8, port: u16) !IpAddress {
34 if (parseIp4(text, port)) |ip4| return ip4 else |err| switch (err) {
3535 error.Overflow,
3636 error.InvalidEnd,
3737 error.InvalidCharacter,
......@@ -40,7 +40,7 @@ pub const IpAddress = union(enum) {
4040 => {},
4141 }
4242
43 return parseIp6(name, port);
43 return parseIp6(text, port);
4444 }
4545
4646 pub fn parseIp4(text: []const u8, port: u16) Ip4Address.ParseError!IpAddress {
......@@ -227,39 +227,102 @@ pub const Ip6Address = struct {
227227 success: Unresolved,
228228 invalid_byte: usize,
229229 unexpected_end,
230 junk_after_end: usize,
231 interface_name_oversized: usize,
230232 };
231233
232 pub fn parse(buffer: []const u8) Parsed {
233 if (buffer.len < 2) return .unexpected_end;
234 pub fn parse(text: []const u8) Parsed {
235 if (text.len < 2) return .unexpected_end;
236 // Has to be u16 elements to handle 3-digit hex numbers from compression.
234237 var parts: [8]u16 = @splat(0);
235 var parts_i: usize = 0;
236 var i: usize = 0;
237 var digit_i: usize = 0;
238 const State = union(enum) { digit, colon, end };
238 var parts_i: u8 = 0;
239 var text_i: u8 = 0;
240 var digit_i: u8 = 0;
241 var compress_start: ?u8 = null;
242 var interface_name_text: ?[]const u8 = null;
243 const State = union(enum) { digit, end };
239244 state: switch (State.digit) {
240 .digit => c: switch (buffer[i]) {
245 .digit => c: switch (text[text_i]) {
241246 'a'...'f' => |c| {
242 const digit = c - 'a';
247 const digit = c - 'a' + 10;
243248 parts[parts_i] = parts[parts_i] * 16 + digit;
244 if (digit_i == 3) {
245 digit_i = 0;
249 if (digit_i == 4) return .{ .invalid_byte = text_i };
250 digit_i += 1;
251 text_i += 1;
252 if (text.len - text_i == 0) {
246253 parts_i += 1;
247 i += 1;
248 if (parts.len - parts_i == 0) continue :state .end;
249 continue :state .colon;
254 continue :state .end;
250255 }
256 continue :c text[text_i];
257 },
258 'A'...'F' => |c| continue :c c - 'A' + 'a',
259 '0'...'9' => |c| {
260 const digit = c - '0';
261 parts[parts_i] = parts[parts_i] * 16 + digit;
262 if (digit_i == 4) return .{ .invalid_byte = text_i };
251263 digit_i += 1;
252 if (buffer.len - i == 0) return .unexpected_end;
253 i += 1;
254 continue :c buffer[i];
264 text_i += 1;
265 if (text.len - text_i == 0) {
266 parts_i += 1;
267 continue :state .end;
268 }
269 continue :c text[text_i];
270 },
271 ':' => {
272 if (digit_i == 0) {
273 if (compress_start != null) return .{ .invalid_byte = text_i };
274 if (text_i == 0) {
275 text_i += 1;
276 if (text[text_i] != ':') return .{ .invalid_byte = text_i };
277 assert(parts_i == 0);
278 }
279 text_i += 1;
280 if (text.len - text_i == 0) return .unexpected_end;
281 compress_start = parts_i;
282 continue :c text[text_i];
283 } else {
284 parts_i += 1;
285 if (parts.len - parts_i == 0) continue :state .end;
286 digit_i = 0;
287 text_i += 1;
288 if (text.len - text_i == 0) return .unexpected_end;
289 continue :c text[text_i];
290 }
291 },
292 '%' => {
293 if (digit_i == 0) return .{ .invalid_byte = text_i };
294 parts_i += 1;
295 text_i += 1;
296 const name = text[text_i..];
297 if (name.len > Interface.Name.max_len) return .{ .interface_name_oversized = text_i };
298 interface_name_text = name;
299 text_i = @intCast(text.len);
300 continue :state .end;
255301 },
256 'A'...'F' => |c| continue :c c + ('a' - 'A'),
257 '0'...'9' => |c| continue :c c + ('a' - '0'),
258 ':' => @panic("TODO"),
259 else => return .{ .invalid_byte = i },
302 else => return .{ .invalid_byte = text_i },
303 },
304 .end => {
305 if (text.len - text_i != 0) return .{ .junk_after_end = text_i };
306 const remaining = parts.len - parts_i;
307 if (compress_start) |s| {
308 const src = parts[s..parts_i];
309 @memmove(parts[parts.len - src.len ..], src);
310 @memset(parts[s..][0..remaining], 0);
311 } else {
312 if (remaining != 0) return .unexpected_end;
313 }
314
315 // Workaround that can be removed when this proposal is
316 // implemented https://github.com/ziglang/zig/issues/19755
317 if ((comptime @import("builtin").cpu.arch.endian()) != .big) {
318 for (&parts) |*part| part.* = @byteSwap(part.*);
319 }
320
321 return .{ .success = .{
322 .bytes = @bitCast(parts),
323 .interface_name = if (interface_name_text) |t| .fromSliceUnchecked(t) else null,
324 } };
260325 },
261 .colon => @panic("TODO"),
262 .end => @panic("TODO"),
263326 }
264327 }
265328
......@@ -319,7 +382,6 @@ pub const Ip6Address = struct {
319382 longest_len = 0;
320383 }
321384
322 try w.writeAll("[");
323385 var i: usize = 0;
324386 var abbrv = false;
325387 while (i < parts.len) : (i += 1) {
......@@ -525,6 +587,12 @@ pub const Interface = struct {
525587
526588 pub fn fromSlice(bytes: []const u8) error{NameTooLong}!Name {
527589 if (bytes.len > max_len) return error.NameTooLong;
590 return .fromSliceUnchecked(bytes);
591 }
592
593 /// Asserts bytes.len fits in `max_len`.
594 pub fn fromSliceUnchecked(bytes: []const u8) Name {
595 assert(bytes.len <= max_len);
528596 var result: Name = undefined;
529597 @memcpy(result.bytes[0..bytes.len], bytes);
530598 result.bytes[bytes.len] = 0;
......@@ -673,6 +741,24 @@ pub const Server = struct {
673741 }
674742};
675743
744test "parsing IPv6 addresses" {
745 try testIp6Parse("fe80::e0e:76ff:fed4:cf22%eno1");
746 try testIp6Parse("2001:db8::1");
747}
748
749fn testIp6Parse(text: []const u8) !void {
750 const ua = switch (Ip6Address.Unresolved.parse(text)) {
751 .success => |p| p,
752 else => |x| {
753 std.debug.print("failed to parse \"{s}\": {any}\n", .{ text, x });
754 return error.TestFailed;
755 },
756 };
757 var buffer: [100]u8 = undefined;
758 const result = try std.fmt.bufPrint(&buffer, "{f}", .{ua});
759 try std.testing.expectEqualStrings(text, result);
760}
761
676762test {
677763 _ = HostName;
678764}
lib/std/Io/net/HostName.zig+3-3
......@@ -612,7 +612,7 @@ pub const ResolvConf = struct {
612612 rc.nameservers_len += 1;
613613 }
614614
615 fn nameservers(rc: *const ResolvConf) []IpAddress {
615 fn nameservers(rc: *const ResolvConf) []const IpAddress {
616616 return rc.nameservers_buffer[0..rc.nameservers_len];
617617 }
618618
......@@ -651,6 +651,6 @@ test ResolvConf {
651651 .attempts = 2,
652652 };
653653
654 try rc.parse(&reader);
655 try std.testing.expect(false);
654 try rc.parse(std.testing.io, &reader);
655 try std.testing.expectEqual(3, rc.nameservers().len);
656656}
lib/std/testing.zig+1-1
......@@ -28,7 +28,7 @@ pub var allocator_instance: std.heap.GeneralPurposeAllocator(.{
2828 break :b .init;
2929};
3030
31pub var io_instance: std.Io.ThreadPool = undefined;
31pub var io_instance: std.Io.Threaded = undefined;
3232pub const io = io_instance.io();
3333
3434/// TODO https://github.com/ziglang/zig/issues/5738