authorgravatar for blblack@gmail.comBrandon L Black <blblack@gmail.com> 2026-10-06 10:37:05-05:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-10-10 01:23:15+02:00
logf8c073d0b12a6d81cbd9584e6f040567fa867032
tree65e51f64f5e5db504644a85feeafe641a8e6a19e
parent36173dcccf52f909ebb02f81ffcfc9e38f457a50

Io.net: expand IPv6:IPv4 formatting and parsing

The IPv6 formatter code now emits the :IPv4 alternate text form for both "IPv4 Mapped" and "Well-Known Prefix" (RFC 6052). The IPv6 parser now accepts the :IPv4 form regardless of what the first 12 bytes are, so long as there is no trailing "%interface" (which should only be useful for link-locals anyways). RFC 4291 does not require any special prefix for the :IPv4 form on the parsing side, and there are many non-canonical cases in the real world which should still parse correctly.

2 files changed, 87 insertions(+), 61 deletions(-)

lib/std/Io/net.zig+83-59
......@@ -501,18 +501,34 @@ pub const Ip6Address = struct {
501501 overflow: usize,
502502 };
503503
504 pub fn parse(text: []const u8) Parsed {
504 pub fn parse(text_in: []const u8) Parsed {
505 var text: []const u8 = text_in; // so we can alias v4_amended if needed
505506 if (text.len < 2) return .incomplete;
506 const ip4_prefix = "::ffff:";
507 if (std.ascii.startsWithIgnoreCase(text, ip4_prefix)) {
508 const parsed = Ip4Address.parse(text[ip4_prefix.len..], 0) catch
509 return .{ .invalid_ip4_mapping = ip4_prefix.len };
510 const b = parsed.bytes;
511 return .{ .success = .{
512 .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, b[0], b[1], b[2], b[3] },
513 .interface_name = null,
514 } };
507
508 // Pre-processing for trailing :IPv4 - If there is no "%iface", and
509 // the part after the last ':' has a '.', parse it as ipv4 and
510 // convert to IPv6 ASCII text in v4_amended as the new "text"
511 var v4_amended: [(8 * 4) + 7]u8 = undefined;
512 if (std.mem.findScalar(u8, text, '%') == null) {
513 const first_parts, const last_part = std.mem.cutScalarLast(u8, text, ':') orelse
514 return .incomplete;
515 if (std.mem.findScalar(u8, last_part, '.')) |_| {
516 if (first_parts.len > (6 * 4) + 5) // hhhh:hhhh:hhhh:hhhh:hhhh:hhhh
517 return .{ .invalid_ip4_mapping = first_parts.len };
518 const parsed = Ip4Address.parse(last_part, 0) catch
519 return .{ .invalid_ip4_mapping = first_parts.len + 1 };
520 @memcpy(v4_amended[0..first_parts.len], first_parts);
521 const v4_part = v4_amended[first_parts.len..][0..10];
522 v4_part[0] = ':';
523 @memcpy(v4_part[1..3], &std.fmt.hex(parsed.bytes[0]));
524 @memcpy(v4_part[3..5], &std.fmt.hex(parsed.bytes[1]));
525 v4_part[5] = ':';
526 @memcpy(v4_part[6..8], &std.fmt.hex(parsed.bytes[2]));
527 @memcpy(v4_part[8..10], &std.fmt.hex(parsed.bytes[3]));
528 text = v4_amended[0 .. first_parts.len + 10];
529 }
515530 }
531
516532 // Has to be u16 elements to handle 3-digit hex numbers from compression.
517533 var parts: [8]u16 = @splat(0);
518534 var parts_i: u8 = 0;
......@@ -608,59 +624,60 @@ pub const Ip6Address = struct {
608624
609625 pub fn format(u: *const Unresolved, w: *Io.Writer) Io.Writer.Error!void {
610626 const bytes = &u.bytes;
611 if (std.mem.eql(u8, bytes[0..12], &[_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff })) {
612 try w.print("::ffff:{d}.{d}.{d}.{d}", .{ bytes[12], bytes[13], bytes[14], bytes[15] });
613 } else {
614 const parts: [8]u16 = .{
615 std.mem.readInt(u16, bytes[0..2], .big),
616 std.mem.readInt(u16, bytes[2..4], .big),
617 std.mem.readInt(u16, bytes[4..6], .big),
618 std.mem.readInt(u16, bytes[6..8], .big),
619 std.mem.readInt(u16, bytes[8..10], .big),
620 std.mem.readInt(u16, bytes[10..12], .big),
621 std.mem.readInt(u16, bytes[12..14], .big),
622 std.mem.readInt(u16, bytes[14..16], .big),
623 };
624
625 // Find the longest zero run
626 var longest_start: usize = 8;
627 var longest_len: usize = 0;
628 var current_start: usize = 0;
629 var current_len: usize = 0;
630
631 for (parts, 0..) |part, i| {
632 if (part == 0) {
633 if (current_len == 0) {
634 current_start = i;
635 }
636 current_len += 1;
637 if (current_len > longest_len) {
638 longest_start = current_start;
639 longest_len = current_len;
640 }
641 } else {
642 current_len = 0;
643 }
644 }
627 if (std.mem.eql(u8, bytes[0..12], &[_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff }))
628 return w.print("::ffff:{d}.{d}.{d}.{d}", .{ bytes[12], bytes[13], bytes[14], bytes[15] });
629 if (std.mem.eql(u8, bytes[0..12], &[_]u8{ 0, 0x64, 0xff, 0x9b, 0, 0, 0, 0, 0, 0, 0, 0 }))
630 return w.print("64:ff9b::{d}.{d}.{d}.{d}", .{ bytes[12], bytes[13], bytes[14], bytes[15] });
631
632 const parts: [8]u16 = .{
633 std.mem.readInt(u16, bytes[0..2], .big),
634 std.mem.readInt(u16, bytes[2..4], .big),
635 std.mem.readInt(u16, bytes[4..6], .big),
636 std.mem.readInt(u16, bytes[6..8], .big),
637 std.mem.readInt(u16, bytes[8..10], .big),
638 std.mem.readInt(u16, bytes[10..12], .big),
639 std.mem.readInt(u16, bytes[12..14], .big),
640 std.mem.readInt(u16, bytes[14..16], .big),
641 };
645642
646 // Only compress if the longest zero run is 2 or more
647 if (longest_len < 2) {
648 longest_start = 8;
649 longest_len = 0;
650 }
643 // Find the longest zero run
644 var longest_start: usize = 8;
645 var longest_len: usize = 0;
646 var current_start: usize = 0;
647 var current_len: usize = 0;
651648
652 var i: usize = 0;
653 while (parts.len - i != 0) : (i += 1) {
654 if (i == longest_start) {
655 // Emit "::" for the longest zero run
656 try w.writeAll(if (i == 0) "::" else ":");
657 i += longest_len - 1; // Skip the compressed range
658 continue;
649 for (parts, 0..) |part, i| {
650 if (part == 0) {
651 if (current_len == 0) {
652 current_start = i;
659653 }
660 try w.print("{x}", .{parts[i]});
661 if (i != parts.len - 1) {
662 try w.writeAll(":");
654 current_len += 1;
655 if (current_len > longest_len) {
656 longest_start = current_start;
657 longest_len = current_len;
663658 }
659 } else {
660 current_len = 0;
661 }
662 }
663
664 // Only compress if the longest zero run is 2 or more
665 if (longest_len < 2) {
666 longest_start = 8;
667 longest_len = 0;
668 }
669
670 var i: usize = 0;
671 while (parts.len - i != 0) : (i += 1) {
672 if (i == longest_start) {
673 // Emit "::" for the longest zero run
674 try w.writeAll(if (i == 0) "::" else ":");
675 i += longest_len - 1; // Skip the compressed range
676 continue;
677 }
678 try w.print("{x}", .{parts[i]});
679 if (i != parts.len - 1) {
680 try w.writeAll(":");
664681 }
665682 }
666683 if (u.interface_name) |n| try w.print("%{s}", .{n});
......@@ -1582,6 +1599,13 @@ test "parsing IPv6 addresses" {
15821599 try testIp6Parse("fe80::abcd:ef12%3");
15831600 try testIp6Parse("ff02::");
15841601 try testIp6Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
1602 try testIp6Parse("::ffff:192.0.2.1"); // IPv4 Mapped
1603 try testIp6Parse("64:ff9b::192.0.2.1"); // RFC 6052 Well-Known Prefix
1604 try testIp6Parse("fe80::e0e:76ff:fed4:cf22%iface.123"); // edge case for :ipv4 parsing
1605 try testIp6ParseTransform("::c000:201", "::192.0.2.1"); // Deprecated "IPv4 Compatible"
1606 try testIp6ParseTransform("::ffff:192.0.2.1", "::ffff:c000:201"); // Non-canonical in Mapped
1607 try testIp6ParseTransform("2001:db8::c000:201", "2001:db8::192.0.2.1"); // arbitrary prefix
1608 try testIp6ParseTransform("2001:db8::201", "2001:db8::0.0.2.1"); // Compression+:IPv4 edge case
15851609}
15861610test "IpAddress.setPort works" {
15871611 var addr: IpAddress = .{ .ip4 = undefined };
lib/std/Io/net/test.zig+4-2
......@@ -16,7 +16,7 @@ test "parse and render IP addresses at comptime" {
1616 const ipv4addr = net.IpAddress.parse("127.0.0.1", 0) catch unreachable;
1717 try testing.expectFmt("127.0.0.1:0", "{f}", .{ipv4addr});
1818
19 try testing.expectError(error.ParseFailed, net.IpAddress.parse("::123.123.123.123", 0));
19 try testing.expectError(error.ParseFailed, net.IpAddress.parse("1::2::123.123.123.123", 0));
2020 try testing.expectError(error.ParseFailed, net.IpAddress.parse("127.01.0.1", 0));
2121 }
2222}
......@@ -59,7 +59,9 @@ test "parse and render IPv6 addresses" {
5959 try testParseAndRenderIp6Address("::1234:5678", "::1234:5678");
6060 try testParseAndRenderIp6Address("2001:db8::1234:5678", "2001:db8::1234:5678");
6161 try testParseAndRenderIp6Address("FF01::FB%1234", "ff01::fb%1234");
62 try testParseAndRenderIp6Address("::123.5.123.5", "::7b05:7b05");
6263 try testParseAndRenderIp6Address("::ffff:123.5.123.5", "::ffff:123.5.123.5");
64 try testParseAndRenderIp6Address("64:ff9b::123.5.123.5", "64:ff9b::123.5.123.5");
6365 try testParseAndRenderIp6Address("ff01::fb%12345678901234", "ff01::fb%12345678901234");
6466}
6567
......@@ -80,7 +82,7 @@ test "IPv6 address parse failures" {
8082 try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 9 }, Unresolved.parse("FF01::Fb:zig"));
8183 try testing.expectEqual(Unresolved.Parsed{ .junk_after_end = 19 }, Unresolved.parse("FF01:0:0:0:0:0:0:FB:"));
8284 try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("FF01:"));
83 try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 5 }, Unresolved.parse("::123.123.123.123"));
85 try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 5 }, Unresolved.parse("1::2::123.123.123.123"));
8486 try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("1"));
8587 try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("ff01::fb%"));
8688}