Commit 539808239e

Andrew Kelley <andrew@ziglang.org>
2025-10-14 03:59:40
std.net: IPv6 parsing fixes
1 parent d3f0c46
Changed files (2)
lib
std
lib/std/Io/net/test.zig
@@ -56,6 +56,7 @@ test "parse and render IPv6 addresses" {
     try testParseAndRenderIp6Address("2001:db8::1234:5678", "2001:db8::1234:5678");
     try testParseAndRenderIp6Address("FF01::FB%1234", "ff01::fb%1234");
     try testParseAndRenderIp6Address("::ffff:123.5.123.5", "::ffff:123.5.123.5");
+    try testParseAndRenderIp6Address("ff01::fb%12345678901234", "ff01::fb%12345678901234");
 }
 
 fn testParseAndRenderIp6Address(input: []const u8, expected_output: []const u8) !void {
@@ -66,16 +67,19 @@ fn testParseAndRenderIp6Address(input: []const u8, expected_output: []const u8)
 }
 
 test "IPv6 address parse failures" {
-    try testing.expectError(error.InvalidCharacter, net.IpAddress.parseIp6(":::", 0));
-    try testing.expectError(error.Overflow, net.IpAddress.parseIp6("FF001::FB", 0));
-    try testing.expectError(error.InvalidCharacter, net.IpAddress.parseIp6("FF01::Fb:zig", 0));
-    try testing.expectError(error.InvalidEnd, net.IpAddress.parseIp6("FF01:0:0:0:0:0:0:FB:", 0));
-    try testing.expectError(error.Incomplete, net.IpAddress.parseIp6("FF01:", 0));
-    try testing.expectError(error.InvalidIpv4Mapping, net.IpAddress.parseIp6("::123.123.123.123", 0));
-    try testing.expectError(error.Incomplete, net.IpAddress.parseIp6("1", 0));
-    try testing.expectError(error.Incomplete, net.IpAddress.parseIp6("ff01::fb%", 0));
-    try testing.expectError(error.Overflow, net.IpAddress.parseIp6("ff01::fb%wlp3" ++ "s0" ** @divExact(std.posix.IFNAMESIZE - 4, 2), 0));
-    try testing.expectError(error.Overflow, net.IpAddress.parseIp6("ff01::fb%12345678901234", 0));
+    try testing.expectError(error.ParseFailed, net.IpAddress.parseIp6(":::", 0));
+
+    const Unresolved = net.Ip6Address.Unresolved;
+
+    try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 2 }, Unresolved.parse(":::"));
+    try testing.expectEqual(Unresolved.Parsed{ .overflow = 4 }, Unresolved.parse("FF001::FB"));
+    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.incomplete, Unresolved.parse("FF01:"));
+    try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 5 }, Unresolved.parse("::123.123.123.123"));
+    try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("1"));
+    try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("ff01::fb%"));
+    try testing.expectEqual(Unresolved.Parsed{ .interface_name_oversized = 9 }, Unresolved.parse("ff01::fb%wlp3" ++ "s0" ** @divExact(std.posix.IFNAMESIZE - 4, 2)));
 }
 
 test "invalid but parseable IPv6 scope ids" {
lib/std/Io/net.zig
@@ -70,7 +70,7 @@ pub const IpAddress = union(enum) {
     pub fn parseLiteral(text: []const u8) ParseLiteralError!IpAddress {
         if (text.len == 0) return error.InvalidAddress;
         if (text[0] == '[') {
-            const addr_end = std.mem.indexOfScalar(u8, text, ']') orelse
+            const addr_end = std.mem.findScalar(u8, text, ']') orelse
                 return error.InvalidAddress;
             const addr_text = text[1..addr_end];
             const port: u16 = p: {
@@ -80,7 +80,7 @@ pub const IpAddress = union(enum) {
             };
             return parseIp6(addr_text, port) catch error.InvalidAddress;
         }
-        if (std.mem.indexOfScalar(u8, text, ':')) |i| {
+        if (std.mem.findScalar(u8, text, ':')) |i| {
             const addr = Ip4Address.parse(text[0..i], 0) catch return error.InvalidAddress;
             return .{ .ip4 = .{
                 .bytes = addr.bytes,
@@ -431,17 +431,22 @@ pub const Ip6Address = struct {
         pub const Parsed = union(enum) {
             success: Unresolved,
             invalid_byte: usize,
-            unexpected_end,
+            incomplete,
             junk_after_end: usize,
             interface_name_oversized: usize,
+            invalid_ip4_mapping: usize,
+            overflow: usize,
         };
 
         pub fn parse(text: []const u8) Parsed {
-            if (text.len < 2) return .unexpected_end;
-            if (std.ascii.startsWithIgnoreCase(text, "::ffff:")) ip4_mapped: {
-                const a4 = (Ip4Address.parse(text["::ffff:".len..], 0) catch break :ip4_mapped).bytes;
+            if (text.len < 2) return .incomplete;
+            const ip4_prefix = "::ffff:";
+            if (std.ascii.startsWithIgnoreCase(text, ip4_prefix)) {
+                const parsed = Ip4Address.parse(text[ip4_prefix.len..], 0) catch
+                    return .{ .invalid_ip4_mapping = ip4_prefix.len };
+                const b = parsed.bytes;
                 return .{ .success = .{
-                    .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, a4[0], a4[1], a4[2], a4[3] },
+                    .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, b[0], b[1], b[2], b[3] },
                     .interface_name = null,
                 } };
             }
@@ -457,7 +462,9 @@ pub const Ip6Address = struct {
                 .digit => c: switch (text[text_i]) {
                     'a'...'f' => |c| {
                         const digit = c - 'a' + 10;
-                        parts[parts_i] = parts[parts_i] * 16 + digit;
+                        parts[parts_i] = (std.math.mul(u16, parts[parts_i], 16) catch return .{
+                            .overflow = text_i,
+                        }) + digit;
                         if (digit_i == 4) return .{ .invalid_byte = text_i };
                         digit_i += 1;
                         text_i += 1;
@@ -470,7 +477,9 @@ pub const Ip6Address = struct {
                     'A'...'F' => |c| continue :c c - 'A' + 'a',
                     '0'...'9' => |c| {
                         const digit = c - '0';
-                        parts[parts_i] = parts[parts_i] * 16 + digit;
+                        parts[parts_i] = (std.math.mul(u16, parts[parts_i], 16) catch return .{
+                            .overflow = text_i,
+                        }) + digit;
                         if (digit_i == 4) return .{ .invalid_byte = text_i };
                         digit_i += 1;
                         text_i += 1;
@@ -497,7 +506,7 @@ pub const Ip6Address = struct {
                             if (parts.len - parts_i == 0) continue :state .end;
                             digit_i = 0;
                             text_i += 1;
-                            if (text.len - text_i == 0) return .unexpected_end;
+                            if (text.len - text_i == 0) return .incomplete;
                             continue :c text[text_i];
                         }
                     },
@@ -507,6 +516,7 @@ pub const Ip6Address = struct {
                         text_i += 1;
                         const name = text[text_i..];
                         if (name.len > Interface.Name.max_len) return .{ .interface_name_oversized = text_i };
+                        if (name.len == 0) return .incomplete;
                         interface_name_text = name;
                         text_i = @intCast(text.len);
                         continue :state .end;
@@ -521,7 +531,7 @@ pub const Ip6Address = struct {
                         @memmove(parts[parts.len - src.len ..], src);
                         @memset(parts[s..][0..remaining], 0);
                     } else {
-                        if (remaining != 0) return .unexpected_end;
+                        if (remaining != 0) return .incomplete;
                     }
 
                     // Workaround that can be removed when this proposal is