Skip to content

Commit 817b0a3

Browse files
committed
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.
1 parent 26db54d commit 817b0a3

File tree

2 files changed

+92
-67
lines changed

2 files changed

+92
-67
lines changed

lib/std/Io/net.zig

Lines changed: 88 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -462,18 +462,34 @@ pub const Ip6Address = struct {
462462
overflow: usize,
463463
};
464464

465-
pub fn parse(text: []const u8) Parsed {
465+
pub fn parse(text_in: []const u8) Parsed {
466+
var text: []const u8 = text_in; // so we can alias v4_amended if needed
466467
if (text.len < 2) return .incomplete;
467-
const ip4_prefix = "::ffff:";
468-
if (std.ascii.startsWithIgnoreCase(text, ip4_prefix)) {
469-
const parsed = Ip4Address.parse(text[ip4_prefix.len..], 0) catch
470-
return .{ .invalid_ip4_mapping = ip4_prefix.len };
471-
const b = parsed.bytes;
472-
return .{ .success = .{
473-
.bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, b[0], b[1], b[2], b[3] },
474-
.interface_name = null,
475-
} };
468+
469+
// Pre-processing for trailing :IPv4 - If there is no "%iface", and
470+
// the part after the last ':' has a '.', parse it as ipv4 and
471+
// convert to IPv6 ASCII text in v4_amended as the new "text"
472+
var v4_amended: [(8 * 4) + 7]u8 = undefined;
473+
if (std.mem.findScalar(u8, text, '%') == null) {
474+
const first_parts, const last_part = std.mem.cutScalarLast(u8, text, ':') orelse
475+
return .incomplete;
476+
if (std.mem.findScalar(u8, last_part, '.')) |_| {
477+
if (first_parts.len > (6 * 4) + 5) // hhhh:hhhh:hhhh:hhhh:hhhh:hhhh
478+
return .{ .invalid_ip4_mapping = first_parts.len };
479+
const parsed = Ip4Address.parse(last_part, 0) catch
480+
return .{ .invalid_ip4_mapping = first_parts.len + 1 };
481+
@memcpy(v4_amended[0..first_parts.len], first_parts);
482+
const v4_part = v4_amended[first_parts.len..][0..10];
483+
v4_part[0] = ':';
484+
@memcpy(v4_part[1..3], &std.fmt.hex(parsed.bytes[0]));
485+
@memcpy(v4_part[3..5], &std.fmt.hex(parsed.bytes[1]));
486+
v4_part[5] = ':';
487+
@memcpy(v4_part[6..8], &std.fmt.hex(parsed.bytes[2]));
488+
@memcpy(v4_part[8..10], &std.fmt.hex(parsed.bytes[3]));
489+
text = v4_amended[0 .. first_parts.len + 10];
490+
}
476491
}
492+
477493
// Has to be u16 elements to handle 3-digit hex numbers from compression.
478494
var parts: [8]u16 = @splat(0);
479495
var parts_i: u8 = 0;
@@ -586,66 +602,67 @@ pub const Ip6Address = struct {
586602

587603
pub fn format(u: *const Unresolved, w: *Io.Writer) Io.Writer.Error!void {
588604
const bytes = &u.bytes;
589-
if (std.mem.eql(u8, bytes[0..12], &[_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff })) {
590-
try w.print("::ffff:{d}.{d}.{d}.{d}", .{ bytes[12], bytes[13], bytes[14], bytes[15] });
591-
} else {
592-
const parts: [8]u16 = .{
593-
std.mem.readInt(u16, bytes[0..2], .big),
594-
std.mem.readInt(u16, bytes[2..4], .big),
595-
std.mem.readInt(u16, bytes[4..6], .big),
596-
std.mem.readInt(u16, bytes[6..8], .big),
597-
std.mem.readInt(u16, bytes[8..10], .big),
598-
std.mem.readInt(u16, bytes[10..12], .big),
599-
std.mem.readInt(u16, bytes[12..14], .big),
600-
std.mem.readInt(u16, bytes[14..16], .big),
601-
};
602-
603-
// Find the longest zero run
604-
var longest_start: usize = 8;
605-
var longest_len: usize = 0;
606-
var current_start: usize = 0;
607-
var current_len: usize = 0;
608-
609-
for (parts, 0..) |part, i| {
610-
if (part == 0) {
611-
if (current_len == 0) {
612-
current_start = i;
613-
}
614-
current_len += 1;
615-
if (current_len > longest_len) {
616-
longest_start = current_start;
617-
longest_len = current_len;
618-
}
619-
} else {
620-
current_len = 0;
621-
}
622-
}
605+
if (std.mem.eql(u8, bytes[0..12], &[_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff }))
606+
return w.print("::ffff:{d}.{d}.{d}.{d}", .{ bytes[12], bytes[13], bytes[14], bytes[15] });
607+
if (std.mem.eql(u8, bytes[0..12], &[_]u8{ 0, 0x64, 0xff, 0x9b, 0, 0, 0, 0, 0, 0, 0, 0 }))
608+
return w.print("64:ff9b::{d}.{d}.{d}.{d}", .{ bytes[12], bytes[13], bytes[14], bytes[15] });
609+
610+
const parts: [8]u16 = .{
611+
std.mem.readInt(u16, bytes[0..2], .big),
612+
std.mem.readInt(u16, bytes[2..4], .big),
613+
std.mem.readInt(u16, bytes[4..6], .big),
614+
std.mem.readInt(u16, bytes[6..8], .big),
615+
std.mem.readInt(u16, bytes[8..10], .big),
616+
std.mem.readInt(u16, bytes[10..12], .big),
617+
std.mem.readInt(u16, bytes[12..14], .big),
618+
std.mem.readInt(u16, bytes[14..16], .big),
619+
};
623620

624-
// Only compress if the longest zero run is 2 or more
625-
if (longest_len < 2) {
626-
longest_start = 8;
627-
longest_len = 0;
628-
}
621+
// Find the longest zero run
622+
var longest_start: usize = 8;
623+
var longest_len: usize = 0;
624+
var current_start: usize = 0;
625+
var current_len: usize = 0;
629626

630-
var i: usize = 0;
631-
var abbrv = false;
632-
while (i < parts.len) : (i += 1) {
633-
if (i == longest_start) {
634-
// Emit "::" for the longest zero run
635-
if (!abbrv) {
636-
try w.writeAll(if (i == 0) "::" else ":");
637-
abbrv = true;
638-
}
639-
i += longest_len - 1; // Skip the compressed range
640-
continue;
627+
for (parts, 0..) |part, i| {
628+
if (part == 0) {
629+
if (current_len == 0) {
630+
current_start = i;
641631
}
642-
if (abbrv) {
643-
abbrv = false;
632+
current_len += 1;
633+
if (current_len > longest_len) {
634+
longest_start = current_start;
635+
longest_len = current_len;
644636
}
645-
try w.print("{x}", .{parts[i]});
646-
if (i != parts.len - 1) {
647-
try w.writeAll(":");
637+
} else {
638+
current_len = 0;
639+
}
640+
}
641+
642+
// Only compress if the longest zero run is 2 or more
643+
if (longest_len < 2) {
644+
longest_start = 8;
645+
longest_len = 0;
646+
}
647+
648+
var i: usize = 0;
649+
var abbrv = false;
650+
while (i < parts.len) : (i += 1) {
651+
if (i == longest_start) {
652+
// Emit "::" for the longest zero run
653+
if (!abbrv) {
654+
try w.writeAll(if (i == 0) "::" else ":");
655+
abbrv = true;
648656
}
657+
i += longest_len - 1; // Skip the compressed range
658+
continue;
659+
}
660+
if (abbrv) {
661+
abbrv = false;
662+
}
663+
try w.print("{x}", .{parts[i]});
664+
if (i != parts.len - 1) {
665+
try w.writeAll(":");
649666
}
650667
}
651668
if (u.interface_name) |n| try w.print("%{s}", .{n});
@@ -1354,6 +1371,12 @@ test "parsing IPv6 addresses" {
13541371
try testIp6Parse("fe80::abcd:ef12%3");
13551372
try testIp6Parse("ff02::");
13561373
try testIp6Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
1374+
try testIp6Parse("::ffff:192.0.2.1"); // IPv4 Mapped
1375+
try testIp6Parse("64:ff9b::192.0.2.1"); // RFC 6052 Well-Known Prefix
1376+
try testIp6Parse("fe80::e0e:76ff:fed4:cf22%iface.123"); // edge case for :ipv4 parsing
1377+
try testIp6ParseTransform("::c000:201", "::192.0.2.1"); // Deprecated "IPv4 Compatible"
1378+
try testIp6ParseTransform("2001:db8::c000:201", "2001:db8::192.0.2.1"); // arbitrary prefix
1379+
try testIp6ParseTransform("2001:db8::201", "2001:db8::0.0.2.1"); // Compression+:IPv4 edge case
13571380
}
13581381

13591382
fn testIp6Parse(input: []const u8) !void {

lib/std/Io/net/test.zig

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ test "parse and render IP addresses at comptime" {
1414
const ipv4addr = net.IpAddress.parse("127.0.0.1", 0) catch unreachable;
1515
try testing.expectFmt("127.0.0.1:0", "{f}", .{ipv4addr});
1616

17-
try testing.expectError(error.ParseFailed, net.IpAddress.parse("::123.123.123.123", 0));
17+
try testing.expectError(error.ParseFailed, net.IpAddress.parse("1::2::123.123.123.123", 0));
1818
try testing.expectError(error.ParseFailed, net.IpAddress.parse("127.01.0.1", 0));
1919
}
2020
}
@@ -57,7 +57,9 @@ test "parse and render IPv6 addresses" {
5757
try testParseAndRenderIp6Address("::1234:5678", "::1234:5678");
5858
try testParseAndRenderIp6Address("2001:db8::1234:5678", "2001:db8::1234:5678");
5959
try testParseAndRenderIp6Address("FF01::FB%1234", "ff01::fb%1234");
60+
try testParseAndRenderIp6Address("::123.5.123.5", "::7b05:7b05");
6061
try testParseAndRenderIp6Address("::ffff:123.5.123.5", "::ffff:123.5.123.5");
62+
try testParseAndRenderIp6Address("64:ff9b::123.5.123.5", "64:ff9b::123.5.123.5");
6163
try testParseAndRenderIp6Address("ff01::fb%12345678901234", "ff01::fb%12345678901234");
6264
}
6365

@@ -78,7 +80,7 @@ test "IPv6 address parse failures" {
7880
try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 9 }, Unresolved.parse("FF01::Fb:zig"));
7981
try testing.expectEqual(Unresolved.Parsed{ .junk_after_end = 19 }, Unresolved.parse("FF01:0:0:0:0:0:0:FB:"));
8082
try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("FF01:"));
81-
try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 5 }, Unresolved.parse("::123.123.123.123"));
83+
try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 5 }, Unresolved.parse("1::2::123.123.123.123"));
8284
try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("1"));
8385
try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("ff01::fb%"));
8486
}

0 commit comments

Comments
 (0)