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 {...@@ -501,18 +501,34 @@ pub const Ip6Address = struct {
501 overflow: usize,501 overflow: usize,
502 };502 };
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
505 if (text.len < 2) return .incomplete;506 if (text.len < 2) return .incomplete;
506 const ip4_prefix = "::ffff:";507
507 if (std.ascii.startsWithIgnoreCase(text, ip4_prefix)) {508 // Pre-processing for trailing :IPv4 - If there is no "%iface", and
508 const parsed = Ip4Address.parse(text[ip4_prefix.len..], 0) catch509 // the part after the last ':' has a '.', parse it as ipv4 and
509 return .{ .invalid_ip4_mapping = ip4_prefix.len };510 // convert to IPv6 ASCII text in v4_amended as the new "text"
510 const b = parsed.bytes;511 var v4_amended: [(8 * 4) + 7]u8 = undefined;
511 return .{ .success = .{512 if (std.mem.findScalar(u8, text, '%') == null) {
512 .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, b[0], b[1], b[2], b[3] },513 const first_parts, const last_part = std.mem.cutScalarLast(u8, text, ':') orelse
513 .interface_name = null,514 return .incomplete;
514 } };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 }
515 }530 }
531
516 // Has to be u16 elements to handle 3-digit hex numbers from compression.532 // Has to be u16 elements to handle 3-digit hex numbers from compression.
517 var parts: [8]u16 = @splat(0);533 var parts: [8]u16 = @splat(0);
518 var parts_i: u8 = 0;534 var parts_i: u8 = 0;
...@@ -608,59 +624,60 @@ pub const Ip6Address = struct {...@@ -608,59 +624,60 @@ pub const Ip6Address = struct {
608624
609 pub fn format(u: *const Unresolved, w: *Io.Writer) Io.Writer.Error!void {625 pub fn format(u: *const Unresolved, w: *Io.Writer) Io.Writer.Error!void {
610 const bytes = &u.bytes;626 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 })) {627 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] });628 return w.print("::ffff:{d}.{d}.{d}.{d}", .{ bytes[12], bytes[13], bytes[14], bytes[15] });
613 } else {629 if (std.mem.eql(u8, bytes[0..12], &[_]u8{ 0, 0x64, 0xff, 0x9b, 0, 0, 0, 0, 0, 0, 0, 0 }))
614 const parts: [8]u16 = .{630 return w.print("64:ff9b::{d}.{d}.{d}.{d}", .{ bytes[12], bytes[13], bytes[14], bytes[15] });
615 std.mem.readInt(u16, bytes[0..2], .big),631
616 std.mem.readInt(u16, bytes[2..4], .big),632 const parts: [8]u16 = .{
617 std.mem.readInt(u16, bytes[4..6], .big),633 std.mem.readInt(u16, bytes[0..2], .big),
618 std.mem.readInt(u16, bytes[6..8], .big),634 std.mem.readInt(u16, bytes[2..4], .big),
619 std.mem.readInt(u16, bytes[8..10], .big),635 std.mem.readInt(u16, bytes[4..6], .big),
620 std.mem.readInt(u16, bytes[10..12], .big),636 std.mem.readInt(u16, bytes[6..8], .big),
621 std.mem.readInt(u16, bytes[12..14], .big),637 std.mem.readInt(u16, bytes[8..10], .big),
622 std.mem.readInt(u16, bytes[14..16], .big),638 std.mem.readInt(u16, bytes[10..12], .big),
623 };639 std.mem.readInt(u16, bytes[12..14], .big),
624640 std.mem.readInt(u16, bytes[14..16], .big),
625 // Find the longest zero run641 };
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 }
645642
646 // Only compress if the longest zero run is 2 or more643 // Find the longest zero run
647 if (longest_len < 2) {644 var longest_start: usize = 8;
648 longest_start = 8;645 var longest_len: usize = 0;
649 longest_len = 0;646 var current_start: usize = 0;
650 }647 var current_len: usize = 0;
651648
652 var i: usize = 0;649 for (parts, 0..) |part, i| {
653 while (parts.len - i != 0) : (i += 1) {650 if (part == 0) {
654 if (i == longest_start) {651 if (current_len == 0) {
655 // Emit "::" for the longest zero run652 current_start = i;
656 try w.writeAll(if (i == 0) "::" else ":");
657 i += longest_len - 1; // Skip the compressed range
658 continue;
659 }653 }
660 try w.print("{x}", .{parts[i]});654 current_len += 1;
661 if (i != parts.len - 1) {655 if (current_len > longest_len) {
662 try w.writeAll(":");656 longest_start = current_start;
657 longest_len = current_len;
663 }658 }
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(":");
664 }681 }
665 }682 }
666 if (u.interface_name) |n| try w.print("%{s}", .{n});683 if (u.interface_name) |n| try w.print("%{s}", .{n});
...@@ -1582,6 +1599,13 @@ test "parsing IPv6 addresses" {...@@ -1582,6 +1599,13 @@ test "parsing IPv6 addresses" {
1582 try testIp6Parse("fe80::abcd:ef12%3");1599 try testIp6Parse("fe80::abcd:ef12%3");
1583 try testIp6Parse("ff02::");1600 try testIp6Parse("ff02::");
1584 try testIp6Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");1601 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
1585}1609}
1586test "IpAddress.setPort works" {1610test "IpAddress.setPort works" {
1587 var addr: IpAddress = .{ .ip4 = undefined };1611 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" {...@@ -16,7 +16,7 @@ test "parse and render IP addresses at comptime" {
16 const ipv4addr = net.IpAddress.parse("127.0.0.1", 0) catch unreachable;16 const ipv4addr = net.IpAddress.parse("127.0.0.1", 0) catch unreachable;
17 try testing.expectFmt("127.0.0.1:0", "{f}", .{ipv4addr});17 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));
20 try testing.expectError(error.ParseFailed, net.IpAddress.parse("127.01.0.1", 0));20 try testing.expectError(error.ParseFailed, net.IpAddress.parse("127.01.0.1", 0));
21 }21 }
22}22}
...@@ -59,7 +59,9 @@ test "parse and render IPv6 addresses" {...@@ -59,7 +59,9 @@ test "parse and render IPv6 addresses" {
59 try testParseAndRenderIp6Address("::1234:5678", "::1234:5678");59 try testParseAndRenderIp6Address("::1234:5678", "::1234:5678");
60 try testParseAndRenderIp6Address("2001:db8::1234:5678", "2001:db8::1234:5678");60 try testParseAndRenderIp6Address("2001:db8::1234:5678", "2001:db8::1234:5678");
61 try testParseAndRenderIp6Address("FF01::FB%1234", "ff01::fb%1234");61 try testParseAndRenderIp6Address("FF01::FB%1234", "ff01::fb%1234");
62 try testParseAndRenderIp6Address("::123.5.123.5", "::7b05:7b05");
62 try testParseAndRenderIp6Address("::ffff:123.5.123.5", "::ffff:123.5.123.5");63 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");
63 try testParseAndRenderIp6Address("ff01::fb%12345678901234", "ff01::fb%12345678901234");65 try testParseAndRenderIp6Address("ff01::fb%12345678901234", "ff01::fb%12345678901234");
64}66}
6567
...@@ -80,7 +82,7 @@ test "IPv6 address parse failures" {...@@ -80,7 +82,7 @@ test "IPv6 address parse failures" {
80 try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 9 }, Unresolved.parse("FF01::Fb:zig"));82 try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 9 }, Unresolved.parse("FF01::Fb:zig"));
81 try testing.expectEqual(Unresolved.Parsed{ .junk_after_end = 19 }, Unresolved.parse("FF01:0:0:0:0:0:0:FB:"));83 try testing.expectEqual(Unresolved.Parsed{ .junk_after_end = 19 }, Unresolved.parse("FF01:0:0:0:0:0:0:FB:"));
82 try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("FF01:"));84 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"));
84 try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("1"));86 try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("1"));
85 try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("ff01::fb%"));87 try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("ff01::fb%"));
86}88}