const std = @import("../../std.zig");
const builtin = @import("builtin");
const os = std.os;
const fmt = std.fmt;
const mem = std.mem;
const math = std.math;
const testing = std.testing;
const native_os = builtin.os;
const have_ifnamesize = @hasDecl(os.system, "IFNAMESIZE");
pub const ResolveScopeIdError = error{
NameTooLong,
PermissionDenied,
AddressFamilyNotSupported,
ProtocolFamilyNotAvailable,
ProcessFdQuotaExceeded,
SystemFdQuotaExceeded,
SystemResources,
ProtocolNotSupported,
SocketTypeNotSupported,
InterfaceNotFound,
FileSystem,
Unexpected,
};
pub fn resolveScopeId(name: []const u8) ResolveScopeIdError!u32 {
if (have_ifnamesize) {
if (name.len >= os.IFNAMESIZE) return error.NameTooLong;
if (native_os.tag == .windows or comptime native_os.tag.isDarwin()) {
var interface_name: [os.IFNAMESIZE:0]u8 = undefined;
mem.copy(u8, &interface_name, name);
interface_name[name.len] = 0;
const rc = blk: {
if (native_os.tag == .windows) {
break :blk os.windows.ws2_32.if_nametoindex(@ptrCast([*:0]const u8, &interface_name));
} else {
const index = os.system.if_nametoindex(@ptrCast([*:0]const u8, &interface_name));
break :blk @bitCast(u32, index);
}
};
if (rc == 0) {
return error.InterfaceNotFound;
}
return rc;
}
if (native_os.tag == .linux) {
const fd = try os.socket(os.AF.INET, os.SOCK.DGRAM, 0);
defer os.closeSocket(fd);
var f: os.ifreq = undefined;
mem.copy(u8, &f.ifrn.name, name);
f.ifrn.name[name.len] = 0;
try os.ioctl_SIOCGIFINDEX(fd, &f);
return @bitCast(u32, f.ifru.ivalue);
}
}
return error.InterfaceNotFound;
}
pub const IPv4 = extern struct {
pub const Address = extern struct {
host: IPv4,
port: u16,
};
pub const localhost_octets = [_]u8{ 127, 0, 0, 1 };
pub const localhost: IPv4 = .{ .octets = localhost_octets };
pub const unspecified_octets = [_]u8{0} ** 4;
pub const unspecified: IPv4 = .{ .octets = unspecified_octets };
pub const broadcast_octets = [_]u8{255} ** 4;
pub const broadcast: IPv4 = .{ .octets = broadcast_octets };
pub const link_local_prefix = [_]u8{ 169, 254 };
pub const documentation_prefixes = [_][]const u8{
&[_]u8{ 192, 0, 2 },
&[_]u8{ 198, 51, 100 },
&[_]u8{ 203, 0, 113 },
};
octets: [4]u8,
pub fn cmp(self: IPv4, other: IPv4) math.Order {
return mem.order(u8, &self.octets, &other.octets);
}
pub fn eql(self: IPv4, other: IPv4) bool {
return mem.eql(u8, &self.octets, &other.octets);
}
pub fn isLoopback(self: IPv4) bool {
return self.octets[0] == 127;
}
pub fn isUnspecified(self: IPv4) bool {
return mem.eql(u8, &self.octets, &unspecified_octets);
}
pub fn isPrivate(self: IPv4) bool {
return self.octets[0] == 10 or
(self.octets[0] == 172 and self.octets[1] >= 16 and self.octets[1] <= 31) or
(self.octets[0] == 192 and self.octets[1] == 168);
}
pub fn isLinkLocal(self: IPv4) bool {
return mem.startsWith(u8, &self.octets, &link_local_prefix);
}
pub fn isMulticast(self: IPv4) bool {
return self.octets[0] >= 224 and self.octets[0] <= 239;
}
pub fn isBroadcast(self: IPv4) bool {
return mem.eql(u8, &self.octets, &broadcast_octets);
}
pub fn isDocumentation(self: IPv4) bool {
inline for (documentation_prefixes) |prefix| {
if (mem.startsWith(u8, &self.octets, prefix)) {
return true;
}
}
return false;
}
pub fn format(
self: IPv4,
comptime layout: []const u8,
opts: fmt.FormatOptions,
writer: anytype,
) !void {
_ = opts;
if (comptime layout.len != 0 and layout[0] != 's') {
@compileError("Unsupported format specifier for IPv4 type '" ++ layout ++ "'.");
}
try fmt.format(writer, "{}.{}.{}.{}", .{
self.octets[0],
self.octets[1],
self.octets[2],
self.octets[3],
});
}
pub const ParseError = error{
UnexpectedEndOfOctet,
TooManyOctets,
OctetOverflow,
UnexpectedToken,
IncompleteAddress,
};
pub fn parse(buf: []const u8) ParseError!IPv4 {
var octets: [4]u8 = undefined;
var octet: u8 = 0;
var index: u8 = 0;
var saw_any_digits: bool = false;
for (buf) |c| {
switch (c) {
'.' => {
if (!saw_any_digits) return error.UnexpectedEndOfOctet;
if (index == 3) return error.TooManyOctets;
octets[index] = octet;
index += 1;
octet = 0;
saw_any_digits = false;
},
'0'...'9' => {
saw_any_digits = true;
octet = math.mul(u8, octet, 10) catch return error.OctetOverflow;
octet = math.add(u8, octet, c - '0') catch return error.OctetOverflow;
},
else => return error.UnexpectedToken,
}
}
if (index == 3 and saw_any_digits) {
octets[index] = octet;
return IPv4{ .octets = octets };
}
return error.IncompleteAddress;
}
pub fn mapToIPv6(self: IPv4) IPv6 {
var octets: [16]u8 = undefined;
mem.copy(u8, octets[0..12], &IPv6.v4_mapped_prefix);
mem.copy(u8, octets[12..], &self.octets);
return IPv6{ .octets = octets, .scope_id = IPv6.no_scope_id };
}
pub fn toIPv6(self: IPv4) IPv6 {
var octets: [16]u8 = undefined;
mem.set(u8, octets[0..12], 0);
mem.copy(u8, octets[12..], &self.octets);
return IPv6{ .octets = octets, .scope_id = IPv6.no_scope_id };
}
};
pub const IPv6 = extern struct {
pub const Address = extern struct {
host: IPv6,
port: u16,
};
pub const localhost_octets = [_]u8{0} ** 15 ++ [_]u8{0x01};
pub const localhost: IPv6 = .{
.octets = localhost_octets,
.scope_id = no_scope_id,
};
pub const unspecified_octets = [_]u8{0} ** 16;
pub const unspecified: IPv6 = .{
.octets = unspecified_octets,
.scope_id = no_scope_id,
};
pub const v4_mapped_prefix = [_]u8{0} ** 10 ++ [_]u8{0xFF} ** 2;
pub const no_scope_id = math.maxInt(u32);
octets: [16]u8,
scope_id: u32,
pub fn cmp(self: IPv6, other: IPv6) math.Order {
return switch (mem.order(u8, self.octets, other.octets)) {
.eq => math.order(self.scope_id, other.scope_id),
else => |order| order,
};
}
pub fn eql(self: IPv6, other: IPv6) bool {
return self.scope_id == other.scope_id and mem.eql(u8, &self.octets, &other.octets);
}
pub fn isUnspecified(self: IPv6) bool {
return mem.eql(u8, &self.octets, &unspecified_octets);
}
pub fn isLoopback(self: IPv6) bool {
return mem.eql(u8, self.octets[0..3], &[_]u8{ 0, 0, 0 }) and
mem.eql(u8, self.octets[12..], &[_]u8{ 0, 0, 0, 1 });
}
pub fn mapsToIPv4(self: IPv6) bool {
return mem.startsWith(u8, &self.octets, &v4_mapped_prefix);
}
pub fn toIPv4(self: IPv6) ?IPv4 {
if (!self.mapsToIPv4()) return null;
return IPv4{ .octets = self.octets[12..][0..4].* };
}
pub fn isMulticast(self: IPv6) bool {
return self.octets[0] == 0xFF;
}
pub fn isLinkLocal(self: IPv6) bool {
return self.octets[0] == 0xFE and self.octets[1] & 0xC0 == 0x80;
}
pub fn isSiteLocal(self: IPv6) bool {
return self.octets[0] == 0xFE and self.octets[1] & 0xC0 == 0xC0;
}
pub const Scope = enum(u8) {
interface = 1,
link = 2,
realm = 3,
admin = 4,
site = 5,
organization = 8,
global = 14,
unknown = 0xFF,
};
pub fn scope(self: IPv6) Scope {
if (!self.isMulticast()) return .unknown;
return switch (self.octets[0] & 0x0F) {
1 => .interface,
2 => .link,
3 => .realm,
4 => .admin,
5 => .site,
8 => .organization,
14 => .global,
else => .unknown,
};
}
pub fn format(
self: IPv6,
comptime layout: []const u8,
opts: fmt.FormatOptions,
writer: anytype,
) !void {
_ = opts;
const specifier = comptime &[_]u8{if (layout.len == 0) 'x' else switch (layout[0]) {
'x', 'X' => |specifier| specifier,
's' => 'x',
'S' => 'X',
else => @compileError("Unsupported format specifier for IPv6 type '" ++ layout ++ "'."),
}};
if (mem.startsWith(u8, &self.octets, &v4_mapped_prefix)) {
return fmt.format(writer, "::{" ++ specifier ++ "}{" ++ specifier ++ "}:{}.{}.{}.{}", .{
0xFF,
0xFF,
self.octets[12],
self.octets[13],
self.octets[14],
self.octets[15],
});
}
const zero_span: struct { from: usize, to: usize } = span: {
var i: usize = 0;
while (i < self.octets.len) : (i += 2) {
if (self.octets[i] == 0 and self.octets[i + 1] == 0) break;
} else break :span .{ .from = 0, .to = 0 };
const from = i;
while (i < self.octets.len) : (i += 2) {
if (self.octets[i] != 0 or self.octets[i + 1] != 0) break;
}
break :span .{ .from = from, .to = i };
};
var i: usize = 0;
while (i != 16) : (i += 2) {
if (zero_span.from != zero_span.to and i == zero_span.from) {
try writer.writeAll("::");
} else if (i >= zero_span.from and i < zero_span.to) {} else {
if (i != 0 and i != zero_span.to) try writer.writeAll(":");
const val = @as(u16, self.octets[i]) << 8 | self.octets[i + 1];
try fmt.formatIntValue(val, specifier, .{}, writer);
}
}
if (self.scope_id != no_scope_id and self.scope_id != 0) {
try fmt.format(writer, "%{d}", .{self.scope_id});
}
}
pub const ParseError = error{
MalformedV4Mapping,
InterfaceNotFound,
UnknownScopeId,
} || IPv4.ParseError;
pub fn parse(buf: []const u8) ParseError!IPv6 {
if (mem.lastIndexOfScalar(u8, buf, '%')) |index| {
const ip_slice = buf[0..index];
const scope_id_slice = buf[index + 1 ..];
if (scope_id_slice.len == 0) return error.UnknownScopeId;
const scope_id: u32 = switch (scope_id_slice[0]) {
'0'...'9' => fmt.parseInt(u32, scope_id_slice, 10),
else => resolveScopeId(scope_id_slice) catch |err| switch (err) {
error.InterfaceNotFound => return error.InterfaceNotFound,
else => err,
},
} catch return error.UnknownScopeId;
return parseWithScopeID(ip_slice, scope_id);
}
return parseWithScopeID(buf, no_scope_id);
}
pub fn parseWithScopeID(buf: []const u8, scope_id: u32) ParseError!IPv6 {
var octets: [16]u8 = undefined;
var octet: u16 = 0;
var tail: [16]u8 = undefined;
var out: []u8 = &octets;
var index: u8 = 0;
var saw_any_digits: bool = false;
var abbrv: bool = false;
for (buf) |c, i| {
switch (c) {
':' => {
if (!saw_any_digits) {
if (abbrv) return error.UnexpectedToken;
if (i != 0) abbrv = true;
mem.set(u8, out[index..], 0);
out = &tail;
index = 0;
continue;
}
if (index == 14) return error.TooManyOctets;
out[index] = @truncate(u8, octet >> 8);
index += 1;
out[index] = @truncate(u8, octet);
index += 1;
octet = 0;
saw_any_digits = false;
},
'.' => {
if (!abbrv or out[0] != 0xFF and out[1] != 0xFF) {
return error.MalformedV4Mapping;
}
const start_index = mem.lastIndexOfScalar(u8, buf[0..i], ':').? + 1;
const v4 = try IPv4.parse(buf[start_index..]);
octets[10] = 0xFF;
octets[11] = 0xFF;
mem.copy(u8, octets[12..], &v4.octets);
return IPv6{ .octets = octets, .scope_id = scope_id };
},
else => {
saw_any_digits = true;
const digit = fmt.charToDigit(c, 16) catch return error.UnexpectedToken;
octet = math.mul(u16, octet, 16) catch return error.OctetOverflow;
octet = math.add(u16, octet, digit) catch return error.OctetOverflow;
},
}
}
if (!saw_any_digits and !abbrv) {
return error.IncompleteAddress;
}
if (index == 14) {
out[14] = @truncate(u8, octet >> 8);
out[15] = @truncate(u8, octet);
} else {
out[index] = @truncate(u8, octet >> 8);
index += 1;
out[index] = @truncate(u8, octet);
index += 1;
mem.copy(u8, octets[16 - index ..], out[0..index]);
}
return IPv6{ .octets = octets, .scope_id = scope_id };
}
};
test {
testing.refAllDecls(@This());
}
test "ip: convert to and from ipv6" {
try testing.expectFmt("::7f00:1", "{}", .{IPv4.localhost.toIPv6()});
try testing.expect(!IPv4.localhost.toIPv6().mapsToIPv4());
try testing.expectFmt("::ffff:127.0.0.1", "{}", .{IPv4.localhost.mapToIPv6()});
try testing.expect(IPv4.localhost.mapToIPv6().mapsToIPv4());
try testing.expect(IPv4.localhost.toIPv6().toIPv4() == null);
try testing.expectFmt("127.0.0.1", "{?}", .{IPv4.localhost.mapToIPv6().toIPv4()});
}
test "ipv4: parse & format" {
const cases = [_][]const u8{
"0.0.0.0",
"255.255.255.255",
"1.2.3.4",
"123.255.0.91",
"127.0.0.1",
};
for (cases) |case| {
try testing.expectFmt(case, "{}", .{try IPv4.parse(case)});
}
}
test "ipv6: parse & format" {
const inputs = [_][]const u8{
"FF01:0:0:0:0:0:0:FB",
"FF01::Fb",
"::1",
"::",
"2001:db8::",
"::1234:5678",
"2001:db8::1234:5678",
"::ffff:123.5.123.5",
};
const outputs = [_][]const u8{
"ff01::fb",
"ff01::fb",
"::1",
"::",
"2001:db8::",
"::1234:5678",
"2001:db8::1234:5678",
"::ffff:123.5.123.5",
};
for (inputs) |input, i| {
try testing.expectFmt(outputs[i], "{}", .{try IPv6.parse(input)});
}
}
test "ipv6: parse & format addresses with scope ids" {
if (!have_ifnamesize) return error.SkipZigTest;
const iface = if (native_os.tag == .linux)
"lo"
else
"lo0";
const input = "FF01::FB%" ++ iface;
const output = "ff01::fb%1";
const parsed = IPv6.parse(input) catch |err| switch (err) {
error.InterfaceNotFound => return,
else => return err,
};
try testing.expectFmt(output, "{}", .{parsed});
}