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.

Fixes: #25896
Fixes: #25897
This commit is contained in:
Brandon Black 2025-11-05 13:00:00 -06:00
parent cbfa87cbea
commit 3e2778202e
2 changed files with 92 additions and 66 deletions

View file

@ -462,18 +462,34 @@ pub const Ip6Address = struct {
overflow: usize, overflow: usize,
}; };
pub fn parse(text: []const u8) Parsed { pub fn parse(text_in: []const u8) Parsed {
var text: []const u8 = text_in; // so we can alias v4_amended if needed
if (text.len < 2) return .incomplete; if (text.len < 2) return .incomplete;
const ip4_prefix = "::ffff:";
if (std.ascii.startsWithIgnoreCase(text, ip4_prefix)) { // Pre-processing for trailing :IPv4 - If there is no "%iface", and
const parsed = Ip4Address.parse(text[ip4_prefix.len..], 0) catch // the part after the last ':' has a '.', parse it as ipv4 and
return .{ .invalid_ip4_mapping = ip4_prefix.len }; // convert to IPv6 ASCII text in v4_amended as the new "text"
const b = parsed.bytes; var v4_amended: [(8 * 4) + 7]u8 = undefined;
return .{ .success = .{ if (std.mem.findScalar(u8, text, '%') == null) {
.bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, b[0], b[1], b[2], b[3] }, const first_parts, const last_part = std.mem.cutScalarLast(u8, text, ':') orelse
.interface_name = null, return .incomplete;
} }; if (std.mem.findScalar(u8, last_part, '.')) |_| {
if (first_parts.len > (6 * 4) + 5) // hhhh:hhhh:hhhh:hhhh:hhhh:hhhh
return .{ .invalid_ip4_mapping = first_parts.len };
const parsed = Ip4Address.parse(last_part, 0) catch
return .{ .invalid_ip4_mapping = first_parts.len + 1 };
@memcpy(v4_amended[0..first_parts.len], first_parts);
const v4_part = v4_amended[first_parts.len..][0..10];
v4_part[0] = ':';
@memcpy(v4_part[1..3], &std.fmt.hex(parsed.bytes[0]));
@memcpy(v4_part[3..5], &std.fmt.hex(parsed.bytes[1]));
v4_part[5] = ':';
@memcpy(v4_part[6..8], &std.fmt.hex(parsed.bytes[2]));
@memcpy(v4_part[8..10], &std.fmt.hex(parsed.bytes[3]));
text = v4_amended[0 .. first_parts.len + 10];
}
} }
// Has to be u16 elements to handle 3-digit hex numbers from compression. // Has to be u16 elements to handle 3-digit hex numbers from compression.
var parts: [8]u16 = @splat(0); var parts: [8]u16 = @splat(0);
var parts_i: u8 = 0; var parts_i: u8 = 0;
@ -586,66 +602,67 @@ pub const Ip6Address = struct {
pub fn format(u: *const Unresolved, w: *Io.Writer) Io.Writer.Error!void { pub fn format(u: *const Unresolved, w: *Io.Writer) Io.Writer.Error!void {
const bytes = &u.bytes; const bytes = &u.bytes;
if (std.mem.eql(u8, bytes[0..12], &[_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff })) { if (std.mem.eql(u8, bytes[0..12], &[_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff }))
try w.print("::ffff:{d}.{d}.{d}.{d}", .{ bytes[12], bytes[13], bytes[14], bytes[15] }); return w.print("::ffff:{d}.{d}.{d}.{d}", .{ bytes[12], bytes[13], bytes[14], bytes[15] });
} else { if (std.mem.eql(u8, bytes[0..12], &[_]u8{ 0, 0x64, 0xff, 0x9b, 0, 0, 0, 0, 0, 0, 0, 0 }))
const parts: [8]u16 = .{ return w.print("64:ff9b::{d}.{d}.{d}.{d}", .{ bytes[12], bytes[13], bytes[14], bytes[15] });
std.mem.readInt(u16, bytes[0..2], .big),
std.mem.readInt(u16, bytes[2..4], .big),
std.mem.readInt(u16, bytes[4..6], .big),
std.mem.readInt(u16, bytes[6..8], .big),
std.mem.readInt(u16, bytes[8..10], .big),
std.mem.readInt(u16, bytes[10..12], .big),
std.mem.readInt(u16, bytes[12..14], .big),
std.mem.readInt(u16, bytes[14..16], .big),
};
// Find the longest zero run const parts: [8]u16 = .{
var longest_start: usize = 8; std.mem.readInt(u16, bytes[0..2], .big),
var longest_len: usize = 0; std.mem.readInt(u16, bytes[2..4], .big),
var current_start: usize = 0; std.mem.readInt(u16, bytes[4..6], .big),
var current_len: usize = 0; std.mem.readInt(u16, bytes[6..8], .big),
std.mem.readInt(u16, bytes[8..10], .big),
std.mem.readInt(u16, bytes[10..12], .big),
std.mem.readInt(u16, bytes[12..14], .big),
std.mem.readInt(u16, bytes[14..16], .big),
};
for (parts, 0..) |part, i| { // Find the longest zero run
if (part == 0) { var longest_start: usize = 8;
if (current_len == 0) { var longest_len: usize = 0;
current_start = i; var current_start: usize = 0;
} var current_len: usize = 0;
current_len += 1;
if (current_len > longest_len) { for (parts, 0..) |part, i| {
longest_start = current_start; if (part == 0) {
longest_len = current_len; if (current_len == 0) {
} current_start = i;
} else {
current_len = 0;
} }
current_len += 1;
if (current_len > longest_len) {
longest_start = current_start;
longest_len = current_len;
}
} else {
current_len = 0;
} }
}
// Only compress if the longest zero run is 2 or more // Only compress if the longest zero run is 2 or more
if (longest_len < 2) { if (longest_len < 2) {
longest_start = 8; longest_start = 8;
longest_len = 0; longest_len = 0;
}
var i: usize = 0;
var abbrv = false;
while (i < parts.len) : (i += 1) {
if (i == longest_start) {
// Emit "::" for the longest zero run
if (!abbrv) {
try w.writeAll(if (i == 0) "::" else ":");
abbrv = true;
}
i += longest_len - 1; // Skip the compressed range
continue;
} }
if (abbrv) {
var i: usize = 0; abbrv = false;
var abbrv = false; }
while (i < parts.len) : (i += 1) { try w.print("{x}", .{parts[i]});
if (i == longest_start) { if (i != parts.len - 1) {
// Emit "::" for the longest zero run try w.writeAll(":");
if (!abbrv) {
try w.writeAll(if (i == 0) "::" else ":");
abbrv = true;
}
i += longest_len - 1; // Skip the compressed range
continue;
}
if (abbrv) {
abbrv = false;
}
try w.print("{x}", .{parts[i]});
if (i != parts.len - 1) {
try w.writeAll(":");
}
} }
} }
if (u.interface_name) |n| try w.print("%{s}", .{n}); if (u.interface_name) |n| try w.print("%{s}", .{n});
@ -1362,6 +1379,13 @@ test "parsing IPv6 addresses" {
try testIp6Parse("fe80::abcd:ef12%3"); try testIp6Parse("fe80::abcd:ef12%3");
try testIp6Parse("ff02::"); try testIp6Parse("ff02::");
try testIp6Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"); try testIp6Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
try testIp6Parse("::ffff:192.0.2.1"); // IPv4 Mapped
try testIp6Parse("64:ff9b::192.0.2.1"); // RFC 6052 Well-Known Prefix
try testIp6Parse("fe80::e0e:76ff:fed4:cf22%iface.123"); // edge case for :ipv4 parsing
try testIp6ParseTransform("::c000:201", "::192.0.2.1"); // Deprecated "IPv4 Compatible"
try testIp6ParseTransform("::ffff:192.0.2.1", "::ffff:c000:201"); // Non-canonical in Mapped
try testIp6ParseTransform("2001:db8::c000:201", "2001:db8::192.0.2.1"); // arbitrary prefix
try testIp6ParseTransform("2001:db8::201", "2001:db8::0.0.2.1"); // Compression+:IPv4 edge case
} }
fn testIp6Parse(input: []const u8) !void { fn testIp6Parse(input: []const u8) !void {

View file

@ -14,7 +14,7 @@ test "parse and render IP addresses at comptime" {
const ipv4addr = net.IpAddress.parse("127.0.0.1", 0) catch unreachable; const ipv4addr = net.IpAddress.parse("127.0.0.1", 0) catch unreachable;
try testing.expectFmt("127.0.0.1:0", "{f}", .{ipv4addr}); try testing.expectFmt("127.0.0.1:0", "{f}", .{ipv4addr});
try testing.expectError(error.ParseFailed, net.IpAddress.parse("::123.123.123.123", 0)); try testing.expectError(error.ParseFailed, net.IpAddress.parse("1::2::123.123.123.123", 0));
try testing.expectError(error.ParseFailed, net.IpAddress.parse("127.01.0.1", 0)); try testing.expectError(error.ParseFailed, net.IpAddress.parse("127.01.0.1", 0));
} }
} }
@ -57,7 +57,9 @@ test "parse and render IPv6 addresses" {
try testParseAndRenderIp6Address("::1234:5678", "::1234:5678"); try testParseAndRenderIp6Address("::1234:5678", "::1234:5678");
try testParseAndRenderIp6Address("2001:db8::1234:5678", "2001:db8::1234:5678"); try testParseAndRenderIp6Address("2001:db8::1234:5678", "2001:db8::1234:5678");
try testParseAndRenderIp6Address("FF01::FB%1234", "ff01::fb%1234"); try testParseAndRenderIp6Address("FF01::FB%1234", "ff01::fb%1234");
try testParseAndRenderIp6Address("::123.5.123.5", "::7b05:7b05");
try testParseAndRenderIp6Address("::ffff:123.5.123.5", "::ffff:123.5.123.5"); try testParseAndRenderIp6Address("::ffff:123.5.123.5", "::ffff:123.5.123.5");
try testParseAndRenderIp6Address("64:ff9b::123.5.123.5", "64:ff9b::123.5.123.5");
try testParseAndRenderIp6Address("ff01::fb%12345678901234", "ff01::fb%12345678901234"); try testParseAndRenderIp6Address("ff01::fb%12345678901234", "ff01::fb%12345678901234");
} }
@ -78,7 +80,7 @@ test "IPv6 address parse failures" {
try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 9 }, Unresolved.parse("FF01::Fb:zig")); try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 9 }, Unresolved.parse("FF01::Fb:zig"));
try testing.expectEqual(Unresolved.Parsed{ .junk_after_end = 19 }, Unresolved.parse("FF01:0:0:0:0:0:0:FB:")); try testing.expectEqual(Unresolved.Parsed{ .junk_after_end = 19 }, Unresolved.parse("FF01:0:0:0:0:0:0:FB:"));
try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("FF01:")); try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("FF01:"));
try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 5 }, Unresolved.parse("::123.123.123.123")); try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 5 }, Unresolved.parse("1::2::123.123.123.123"));
try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("1")); try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("1"));
try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("ff01::fb%")); try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("ff01::fb%"));
} }