diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0202ace..ae58a2f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,8 @@ jobs: - name: Setup Zig for OpenHarmony uses: openharmony-zig/setup-zig-ohos@v0.1.0 id: setup-zig + with: + tag: "0.16.0" - name: Build run: | diff --git a/.gitignore b/.gitignore index a74a05a..87d3fdb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ .DS_Store .zig-cache +zig-pkg/ zig-out dist/ package.har -package/libs/ \ No newline at end of file +package/libs/ diff --git a/build.zig b/build.zig index 3ea0380..1276dd0 100644 --- a/build.zig +++ b/build.zig @@ -24,4 +24,11 @@ pub fn build(b: *std.Build) !void { if (result.x64) |x64| { x64.root_module.addImport("napi", napi); } + + const dts = try napi_build.generateTypeDefinition(b, .{ + .root_source_file = b.path("./src/lib.zig"), + .output = b.path("package/index.d.ts"), + .napi_module = napi, + }); + b.getInstallStep().dependOn(&dts.step); } diff --git a/build.zig.zon b/build.zig.zon index f403d29..d13ff94 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,9 +5,9 @@ // Tracks the earliest Zig version that the package considers to be a // supported use case. - .minimum_zig_version = "0.15.1", + .minimum_zig_version = "0.16.0", - .dependencies = .{ .@"zig-napi" = .{ .url = "https://github.com/openharmony-zig/zig-addon/archive/refs/tags/0.0.1-beta.2.tar.gz", .hash = "zig_napi-0.0.1-beta.2-H6OwawaxAgDMitwMH0m5ZJZoIpdc5LU2C34P_msTfEQo" } }, + .dependencies = .{ .@"zig-napi" = .{ .url = "https://github.com/openharmony-zig/zig-napi/archive/refs/tags/0.1.0.tar.gz", .hash = "zig_napi-0.1.0-H6Owa7sDBgBLhd-ooFFJIqt3CAGATY4sIYHopmSYkRDP" } }, .paths = .{ "build.zig", diff --git a/package/index.d.ts b/package/index.d.ts index cfd5592..534bd43 100644 --- a/package/index.d.ts +++ b/package/index.d.ts @@ -1,19 +1,20 @@ +/* auto-generated by zig-addon */ +/* eslint-disable */ + export interface PingOption { - count?: number; - interval_ms?: number; - timeout_ms?: number; - ip_version?: string; + count?: number + interval_ms?: number + timeout_ms?: number + ip_version?: string } export interface PingResult { - sequence: number; - rtt_ms: number; - success: boolean; - error_msg?: string; - ip_addr: string; + sequence: number + rtt_ms: number + success: boolean + error_msg?: string + ip_addr: string } -export declare function ping( - host: string, - config?: PingOption -): Promise; + +export declare function ping(host: string, config: PingOption | undefined | null): Promise> diff --git a/src/domain.zig b/src/domain.zig index 01f6531..17db6bf 100644 --- a/src/domain.zig +++ b/src/domain.zig @@ -1,9 +1,15 @@ const std = @import("std"); const napi = @import("napi"); -const net = std.net; +const net = std.Io.net; const posix = std.posix; const Allocator = std.mem.Allocator; +const PosixAddress = extern union { + any: posix.sockaddr, + in: posix.sockaddr.in, + in6: posix.sockaddr.in6, +}; + pub const IPResolverError = error{ InvalidIPFormat, ResolutionFailed, @@ -17,28 +23,33 @@ pub const IPFamily = enum { }; pub fn isValidIP(ip_str: []const u8) bool { - if (net.Address.parseIp4(ip_str, 0)) |_| { + if (net.IpAddress.parseIp4(ip_str, 0)) |_| { return true; } else |_| {} - if (net.Address.parseIp6(ip_str, 0)) |_| { + if (net.IpAddress.parseIp6(ip_str, 0)) |_| { return true; } else |_| {} return false; } -fn formatAddress(allocator: Allocator, addr: std.net.Address) ![]const u8 { +fn formatSockaddr(allocator: Allocator, addr: *const posix.sockaddr) ![]const u8 { var buf: [64]u8 = undefined; var writer = std.Io.Writer.fixed(&buf); + const storage: *const PosixAddress = @ptrCast(@alignCast(addr)); - switch (addr.any.family) { + switch (addr.family) { posix.AF.INET => { - const bytes = std.mem.asBytes(&addr.in.sa.addr); + const bytes: [4]u8 = @bitCast(storage.in.addr); _ = try writer.print("{}.{}.{}.{}", .{ bytes[0], bytes[1], bytes[2], bytes[3] }); }, posix.AF.INET6 => { - _ = try addr.in6.format(&writer); + const ip6 = net.Ip6Address.Unresolved{ + .bytes = storage.in6.addr, + .interface_name = null, + }; + _ = try ip6.format(&writer); }, else => return error.UnsupportedFamily, } @@ -46,38 +57,58 @@ fn formatAddress(allocator: Allocator, addr: std.net.Address) ![]const u8 { return try allocator.dupe(u8, writer.buffered()); } -fn isAddressFamily(addr: std.net.Address, family: IPFamily) bool { +fn isAddressFamily(addr: *const posix.sockaddr, family: IPFamily) bool { return switch (family) { - .ipv4 => addr.any.family == posix.AF.INET, - .ipv6 => addr.any.family == posix.AF.INET6, + .ipv4 => addr.family == posix.AF.INET, + .ipv6 => addr.family == posix.AF.INET6, .auto => true, }; } /// Resolve hostname to IP address string pub fn resolveHostname(allocator: Allocator, hostname: []const u8, prefer: IPFamily) ![]const u8 { - // Use std.net to resolve hostname - const address_list = std.net.getAddressList(allocator, hostname, 0) catch { - return napi.Error.fromReason("Failed to resolve hostname"); - }; - defer address_list.deinit(); + const hostname_z = try allocator.dupeZ(u8, hostname); + defer allocator.free(hostname_z); - if (address_list.addrs.len == 0) { - return napi.Error.fromReason("Resolve hostname's result is empty"); - } + const family: i32 = switch (prefer) { + .ipv4 => @intCast(posix.AF.INET), + .ipv6 => @intCast(posix.AF.INET6), + .auto => @intCast(posix.AF.UNSPEC), + }; + const hints = std.c.addrinfo{ + .flags = .{}, + .family = family, + .socktype = @intCast(posix.SOCK.DGRAM), + .protocol = 0, + .addrlen = 0, + .addr = null, + .canonname = null, + .next = null, + }; - // If auto mode, return the first address - if (prefer == .auto) { - return try formatAddress(allocator, address_list.addrs[0]); + var result: ?*std.c.addrinfo = null; + if (@intFromEnum(std.c.getaddrinfo(hostname_z, null, &hints, &result)) != 0) { + return napi.Error.fromReason("Failed to resolve hostname"); } - - for (address_list.addrs) |addr| { + defer if (result) |res| std.c.freeaddrinfo(res); + + var first_addr: ?*posix.sockaddr = null; + var item = result; + while (item) |info| : (item = info.next) { + const addr = info.addr orelse continue; + if (first_addr == null) { + first_addr = addr; + } if (isAddressFamily(addr, prefer)) { - return try formatAddress(allocator, addr); + return try formatSockaddr(allocator, addr); } } - return try formatAddress(allocator, address_list.addrs[0]); + if (first_addr) |addr| { + return try formatSockaddr(allocator, addr); + } else { + return napi.Error.fromReason("Resolve hostname's result is empty"); + } } /// Main function: get IP address string diff --git a/src/lib.zig b/src/lib.zig index dfa4d4d..36c0028 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -1,18 +1,81 @@ const std = @import("std"); const napi = @import("napi"); -const net = std.net; -const os = std.os; +const net = std.Io.net; const posix = std.posix; const pack = @import("pack.zig"); const domain = @import("domain.zig"); const ArrayList = std.ArrayList; +const PosixAddress = extern union { + any: posix.sockaddr, + in: posix.sockaddr.in, + in6: posix.sockaddr.in6, +}; + +const ParsedAddress = struct { + storage: PosixAddress, + len: posix.socklen_t, + family: posix.sa_family_t, +}; + +fn parseIPAddress(ip_addr: []const u8) !ParsedAddress { + const addr = net.IpAddress.parse(ip_addr, 0) catch return error.InvalidAddress; + + switch (addr) { + .ip4 => |ip4| { + const storage = PosixAddress{ + .in = .{ + .port = std.mem.nativeToBig(u16, ip4.port), + .addr = @bitCast(ip4.bytes), + }, + }; + return .{ + .storage = storage, + .len = @sizeOf(posix.sockaddr.in), + .family = posix.AF.INET, + }; + }, + .ip6 => |ip6| { + const storage = PosixAddress{ + .in6 = .{ + .port = std.mem.nativeToBig(u16, ip6.port), + .flowinfo = ip6.flow, + .addr = ip6.bytes, + .scope_id = ip6.interface.index, + }, + }; + return .{ + .storage = storage, + .len = @sizeOf(posix.sockaddr.in6), + .family = posix.AF.INET6, + }; + }, + } +} + +fn monotonicNanoTimestamp() i128 { + var ts: posix.timespec = undefined; + if (std.c.clock_gettime(.MONOTONIC, &ts) != 0) { + @panic("Failed to get monotonic clock"); + } + return @as(i128, ts.sec) * std.time.ns_per_s + @as(i128, ts.nsec); +} + const PingResult = struct { sequence: u16, rtt_ms: f64, success: bool, error_msg: ?[]const u8, ip_addr: []const u8, + + const Self = @This(); + + pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { + if (self.error_msg) |msg| { + allocator.free(msg); + } + allocator.free(self.ip_addr); + } }; const PingOption = struct { @@ -26,31 +89,65 @@ const InnerPingOption = struct { count: u32, interval_ms: u32, timeout_ms: u32, - ip_version: []const u8, + ip_version: domain.IPFamily, }; -fn ping_execute(_: napi.Env, config: PingConfig) !ArrayList(PingResult) { - const allocator = std.heap.page_allocator; +fn appendPingResult( + allocator: std.mem.Allocator, + results: *ArrayList(PingResult), + sequence: u16, + rtt_ms: f64, + success: bool, + error_msg: ?[]const u8, + ip_addr: []const u8, +) !void { + const owned_error = if (error_msg) |msg| try allocator.dupe(u8, msg) else null; + errdefer if (owned_error) |msg| allocator.free(msg); + + const owned_ip_addr = try allocator.dupe(u8, ip_addr); + errdefer allocator.free(owned_ip_addr); + + try results.append(allocator, PingResult{ + .sequence = sequence, + .rtt_ms = rtt_ms, + .success = success, + .error_msg = owned_error, + .ip_addr = owned_ip_addr, + }); +} + +fn deinitPartialPingResults(allocator: std.mem.Allocator, results: *ArrayList(PingResult)) void { + for (results.items) |*result| { + result.deinit(allocator); + } + results.deinit(allocator); +} - const ip_version = if (std.mem.eql(u8, config.config.ip_version, "ipv4")) domain.IPFamily.ipv4 else if (std.mem.eql(u8, config.config.ip_version, "ipv6")) domain.IPFamily.ipv6 else domain.IPFamily.auto; +fn ping_execute(config: PingConfig) !ArrayList(PingResult) { + const allocator = napi.globalAllocator(); - const target_ip = domain.getIPAddress(allocator, config.host, ip_version) catch { + const target_ip = domain.getIPAddress(allocator, config.host, config.config.ip_version) catch { return napi.Error.fromReason("Failed to get IP address"); }; + defer allocator.free(target_ip); - const target_addr = net.Address.parseIp(target_ip, 0) catch { + const target_addr = parseIPAddress(target_ip) catch { return napi.Error.fromReason("Failed to parse IP address"); }; var results = ArrayList(PingResult).empty; + errdefer deinitPartialPingResults(allocator, &results); - if (target_addr.any.family != posix.AF.INET and target_addr.any.family != posix.AF.INET6) { + if (target_addr.family != posix.AF.INET and target_addr.family != posix.AF.INET6) { return napi.Error.fromReason("IPv4 is not supported"); } - const socket: posix.socket_t = posix.socket(target_addr.any.family, posix.SOCK.DGRAM, posix.IPPROTO.ICMP) catch { + const socket_rc = std.c.socket(@intCast(target_addr.family), @intCast(posix.SOCK.DGRAM), @intCast(posix.IPPROTO.ICMP)); + if (socket_rc < 0) { return napi.Error.fromReason("Failed to create socket"); - }; + } + const socket: posix.socket_t = socket_rc; + defer _ = std.c.close(socket); const timeout_ms = config.config.timeout_ms; const timeout = posix.timeval{ @@ -69,50 +166,65 @@ fn ping_execute(_: napi.Env, config: PingConfig) !ArrayList(PingResult) { for (0..config.config.count) |index| { // Create ICMP packet with auto-generated payload var packet = pack.ICMPPacket.init(allocator, 1, @intCast(index)) catch @panic("Failed to initialize ICMP packet"); + defer allocator.free(packet.data); const packet_data = packet.serialize(allocator) catch { return napi.Error.fromReason("Failed to serialize ICMP packet"); }; - const start_time = std.time.nanoTimestamp(); - _ = posix.sendto(socket, packet_data, 0, &target_addr.any, target_addr.getOsSockLen()) catch @panic("Failed to send data"); + defer allocator.free(packet_data); + + const start_time = monotonicNanoTimestamp(); + if (std.c.sendto(socket, packet_data.ptr, packet_data.len, 0, &target_addr.storage.any, target_addr.len) < 0) { + @panic("Failed to send data"); + } - const bytes_received = posix.recvfrom(socket, buffer[0..], 0, &addr, &addrlen) catch @panic("Failed to receive data"); - const end_time = std.time.nanoTimestamp(); + const recv_rc = std.c.recvfrom(socket, buffer[0..].ptr, buffer.len, 0, &addr, &addrlen); + if (recv_rc < 0) { + @panic("Failed to receive data"); + } + const bytes_received: usize = @intCast(recv_rc); + const end_time = monotonicNanoTimestamp(); const rtt_ns = end_time - start_time; const rtt_ms = @as(f64, @floatFromInt(rtt_ns)) / 1_000_000.0; // Parse the received packet const icmp_data = pack.extractICMPFromIP(buffer[0..bytes_received]) catch { - results.append(allocator, PingResult{ - .sequence = 1, - .rtt_ms = rtt_ms, - .success = false, - .error_msg = "Failed to extract ICMP data from IP packet", - .ip_addr = target_ip, - }) catch @panic("Failed to append PingResult"); + appendPingResult( + allocator, + &results, + 1, + rtt_ms, + false, + "Failed to extract ICMP data from IP packet", + target_ip, + ) catch @panic("Failed to append PingResult"); continue; }; const received_packet = pack.ICMPPacket.parse(allocator, icmp_data) catch { - results.append(allocator, PingResult{ - .sequence = 1, - .rtt_ms = rtt_ms, - .success = false, - .error_msg = "Failed to parse ICMP packet", - .ip_addr = target_ip, - }) catch @panic("Failed to append PingResult"); + appendPingResult( + allocator, + &results, + 1, + rtt_ms, + false, + "Failed to parse ICMP packet", + target_ip, + ) catch @panic("Failed to append PingResult"); continue; }; // Verify checksum if (!received_packet.verifyChecksum()) { - results.append(allocator, PingResult{ - .sequence = 1, - .rtt_ms = rtt_ms, - .success = false, - .error_msg = "ICMP packet checksum verification failed", - .ip_addr = target_ip, - }) catch @panic("Failed to append PingResult"); + appendPingResult( + allocator, + &results, + 1, + rtt_ms, + false, + "ICMP packet checksum verification failed", + target_ip, + ) catch @panic("Failed to append PingResult"); continue; } @@ -120,13 +232,15 @@ fn ping_execute(_: napi.Env, config: PingConfig) !ArrayList(PingResult) { const is_echo_reply = received_packet.header.type == pack.ICMP_ECHO_REPLY; const sequence_match = std.mem.nativeToBig(u16, received_packet.header.sequence) == index; - results.append(allocator, PingResult{ - .sequence = 1, - .rtt_ms = rtt_ms, - .success = is_echo_reply and sequence_match, - .error_msg = null, - .ip_addr = target_ip, - }) catch @panic("Failed to append PingResult"); + appendPingResult( + allocator, + &results, + 1, + rtt_ms, + is_echo_reply and sequence_match, + null, + target_ip, + ) catch @panic("Failed to append PingResult"); } return results; @@ -135,14 +249,20 @@ fn ping_execute(_: napi.Env, config: PingConfig) !ArrayList(PingResult) { const PingConfig = struct { host: []const u8, config: InnerPingOption, + + const Self = @This(); + + pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { + allocator.free(self.host); + } }; -pub fn ping(env: napi.Env, host: []const u8, config: ?PingOption) napi.Promise { +pub fn ping(host: []const u8, config: ?PingOption) napi.Async(ArrayList(PingResult), .thread) { var options = InnerPingOption{ .count = 10, .interval_ms = 1000, .timeout_ms = 5000, - .ip_version = "ipv4", + .ip_version = .ipv4, }; if (config) |c| { @@ -156,19 +276,20 @@ pub fn ping(env: napi.Env, host: []const u8, config: ?PingOption) napi.Promise { options.timeout_ms = timeout_ms; } if (c.ip_version) |ip_version| { - options.ip_version = ip_version; + defer napi.globalAllocator().free(ip_version); + options.ip_version = if (std.mem.eql(u8, ip_version, "ipv4")) + .ipv4 + else if (std.mem.eql(u8, ip_version, "ipv6")) + .ipv6 + else + .auto; } } - const worker = napi.Worker(env, .{ - .data = PingConfig{ - .host = host, - .config = options, - }, - .Execute = ping_execute, - }); - - return worker.AsyncQueue(); + return napi.Async(ArrayList(PingResult), .thread).from(PingConfig{ + .host = host, + .config = options, + }, ping_execute); } comptime { diff --git a/src/pack.zig b/src/pack.zig index 2cc97a0..4f639bb 100644 --- a/src/pack.zig +++ b/src/pack.zig @@ -1,9 +1,4 @@ const std = @import("std"); -const net = std.net; -const os = std.os; -const time = std.time; -const print = std.debug.print; -const ArrayList = std.ArrayList; pub const ICMPHeader = packed struct { type: u8,