From 6d3f2b368ae8dbcd0e433dfc5f3dba8f3ea53b89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 15 May 2026 22:42:33 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20#811=20=E2=80=94=20net.isIP/isIPv4/i?= =?UTF-8?q?sIPv6=20+=20Happy-Eyeballs=20default=20accessors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `net.isIP`, `net.isIPv4`, `net.isIPv6`, `net.getDefaultAutoSelectFamily`, `net.setDefaultAutoSelectFamily`, `net.getDefaultAutoSelectFamilyAttemptTimeout`, and `net.setDefaultAutoSelectFamilyAttemptTimeout` previously returned `undefined` because they had no NATIVE_MODULE_TABLE entries and no perry-ext-net runtime implementations. Adds: - perry-ext-net runtime fns: - `js_net_is_ip` — returns 0/4/6 via `std::net::Ipv4Addr` / `std::net::Ipv6Addr` parse. `is_ipv6_str` rejects bracketed and zone-id (`%`) forms to match Node. - `js_net_is_ipv4` / `js_net_is_ipv6` — boolean. - `js_net_get_default_auto_select_family` / `js_net_set_default_auto_select_family` — read/write `AUTO_SELECT_FAMILY` (AtomicBool, default `true`). - `js_net_get_default_auto_select_family_attempt_timeout` / `js_net_set_default_auto_select_family_attempt_timeout` — read/write `AUTO_SELECT_FAMILY_ATTEMPT_TIMEOUT_MS` (AtomicI32, default 500ms to match Node v20+). - NATIVE_MODULE_TABLE entries dispatch `net.(args)` to the new runtime fns via the standard NA_STR/NA_F64 + NR_F64 protocol (return values are already NaN-boxed JSValues). - perry-api-manifest entries flip the #463 strict-API gate from "not implemented" to "supported". 6-line repro from the issue now matches Node byte-for-byte; full auto-select-family round-trip works. --- crates/perry-api-manifest/src/entries.rs | 9 +++ crates/perry-codegen/src/lower_call.rs | 65 ++++++++++++++++++ crates/perry-ext-net/src/lib.rs | 87 ++++++++++++++++++++++++ 3 files changed, 161 insertions(+) diff --git a/crates/perry-api-manifest/src/entries.rs b/crates/perry-api-manifest/src/entries.rs index 280c5f2a..fc81082f 100644 --- a/crates/perry-api-manifest/src/entries.rs +++ b/crates/perry-api-manifest/src/entries.rs @@ -481,6 +481,15 @@ pub static API_MANIFEST: &[ApiEntry] = &[ method("net", "destroy", true, Some("Socket")), method("net", "on", true, Some("Socket")), method("net", "upgradeToTLS", true, Some("Socket")), + // Issue #811 — IP classification helpers + Happy-Eyeballs default + // accessors. Pure string/global-flag functions. + method("net", "isIP", false, None), + method("net", "isIPv4", false, None), + method("net", "isIPv6", false, None), + method("net", "getDefaultAutoSelectFamily", false, None), + method("net", "setDefaultAutoSelectFamily", false, None), + method("net", "getDefaultAutoSelectFamilyAttemptTimeout", false, None), + method("net", "setDefaultAutoSelectFamilyAttemptTimeout", false, None), method_sig( "tls", "connect", diff --git a/crates/perry-codegen/src/lower_call.rs b/crates/perry-codegen/src/lower_call.rs index b318a89e..3866a872 100644 --- a/crates/perry-codegen/src/lower_call.rs +++ b/crates/perry-codegen/src/lower_call.rs @@ -7006,6 +7006,71 @@ const NATIVE_MODULE_TABLE: &[NativeModSig] = &[ args: &[], ret: NR_PTR, }, + // Issue #810/#811 — IP classification helpers + Happy-Eyeballs default + // accessors. Pure string/global-flag functions, no sockets or I/O. + NativeModSig { + module: "net", + has_receiver: false, + method: "isIP", + class_filter: None, + runtime: "js_net_is_ip", + args: &[NA_STR], + ret: NR_F64, + }, + NativeModSig { + module: "net", + has_receiver: false, + method: "isIPv4", + class_filter: None, + runtime: "js_net_is_ipv4", + args: &[NA_STR], + ret: NR_F64, + }, + NativeModSig { + module: "net", + has_receiver: false, + method: "isIPv6", + class_filter: None, + runtime: "js_net_is_ipv6", + args: &[NA_STR], + ret: NR_F64, + }, + NativeModSig { + module: "net", + has_receiver: false, + method: "getDefaultAutoSelectFamily", + class_filter: None, + runtime: "js_net_get_default_auto_select_family", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "net", + has_receiver: false, + method: "setDefaultAutoSelectFamily", + class_filter: None, + runtime: "js_net_set_default_auto_select_family", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "net", + has_receiver: false, + method: "getDefaultAutoSelectFamilyAttemptTimeout", + class_filter: None, + runtime: "js_net_get_default_auto_select_family_attempt_timeout", + args: &[], + ret: NR_F64, + }, + NativeModSig { + module: "net", + has_receiver: false, + method: "setDefaultAutoSelectFamilyAttemptTimeout", + class_filter: None, + runtime: "js_net_set_default_auto_select_family_attempt_timeout", + args: &[NA_F64], + ret: NR_F64, + }, // Instance method: `sock.connect(port, host)` initiates the deferred // TCP connection on a `new net.Socket()`-allocated handle. Twin of // the `createConnection` factory above — both end up in the same diff --git a/crates/perry-ext-net/src/lib.rs b/crates/perry-ext-net/src/lib.rs index 2d7e298f..d955ca0f 100644 --- a/crates/perry-ext-net/src/lib.rs +++ b/crates/perry-ext-net/src/lib.rs @@ -190,6 +190,93 @@ enum PendingNetEvent { // ─── Helpers ───────────────────────────────────────────────────────────────── +/// Issue #811 — `net.isIP(s)` returns 0/4/6 (number). +fn classify_ip(s: &str) -> i32 { + if is_ipv4_str(s) { + 4 + } else if is_ipv6_str(s) { + 6 + } else { + 0 + } +} + +fn is_ipv4_str(s: &str) -> bool { + s.parse::().is_ok() +} + +fn is_ipv6_str(s: &str) -> bool { + // Node's `net.isIPv6` rejects brackets and zone-id (`%`) suffixes — + // those forms aren't bare addresses. + if s.contains('[') || s.contains(']') || s.contains('%') { + return false; + } + s.parse::().is_ok() +} + +#[no_mangle] +pub unsafe extern "C" fn js_net_is_ip(s_ptr: i64) -> f64 { + let kind = match string_from_header_i64(s_ptr) { + Some(s) => classify_ip(&s), + None => 0, + }; + f64::from_bits(JsValue::from_number(kind as f64).0) +} + +#[no_mangle] +pub unsafe extern "C" fn js_net_is_ipv4(s_ptr: i64) -> f64 { + let is = match string_from_header_i64(s_ptr) { + Some(s) => is_ipv4_str(&s), + None => false, + }; + f64::from_bits(JsValue::from_bool(is).0) +} + +#[no_mangle] +pub unsafe extern "C" fn js_net_is_ipv6(s_ptr: i64) -> f64 { + let is = match string_from_header_i64(s_ptr) { + Some(s) => is_ipv6_str(&s), + None => false, + }; + f64::from_bits(JsValue::from_bool(is).0) +} + +// Happy-Eyeballs (auto-select-family) defaults. Process-wide globals +// that `getDefault*` reads and `setDefault*` writes. +static AUTO_SELECT_FAMILY: std::sync::atomic::AtomicBool = + std::sync::atomic::AtomicBool::new(true); +// Node's current default is 500ms (raised from 250 in v20.x); pin to +// 500 so byte-for-byte parity holds against `node --experimental-strip-types`. +static AUTO_SELECT_FAMILY_ATTEMPT_TIMEOUT_MS: std::sync::atomic::AtomicI32 = + std::sync::atomic::AtomicI32::new(500); + +#[no_mangle] +pub unsafe extern "C" fn js_net_get_default_auto_select_family() -> f64 { + let v = AUTO_SELECT_FAMILY.load(std::sync::atomic::Ordering::Relaxed); + f64::from_bits(JsValue::from_bool(v).0) +} + +#[no_mangle] +pub unsafe extern "C" fn js_net_set_default_auto_select_family(val_f64: f64) -> f64 { + let val = JsValue(val_f64.to_bits()).to_bool(); + AUTO_SELECT_FAMILY.store(val, std::sync::atomic::Ordering::Relaxed); + f64::from_bits(JsValue::UNDEFINED.0) +} + +#[no_mangle] +pub unsafe extern "C" fn js_net_get_default_auto_select_family_attempt_timeout() -> f64 { + let v = AUTO_SELECT_FAMILY_ATTEMPT_TIMEOUT_MS.load(std::sync::atomic::Ordering::Relaxed); + f64::from_bits(JsValue::from_number(v as f64).0) +} + +#[no_mangle] +pub unsafe extern "C" fn js_net_set_default_auto_select_family_attempt_timeout(ms_f64: f64) -> f64 { + let n = JsValue(ms_f64.to_bits()).to_number(); + let ms = if n.is_finite() && n >= 0.0 { n as i32 } else { 0 }; + AUTO_SELECT_FAMILY_ATTEMPT_TIMEOUT_MS.store(ms, std::sync::atomic::Ordering::Relaxed); + f64::from_bits(JsValue::UNDEFINED.0) +} + unsafe fn string_from_header_i64(ptr: i64) -> Option { let p = ptr as usize; if p < 0x1000 { From 2462461803d733e87fc7c54f90dd8d0ca978ef96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 16 May 2026 08:19:43 +0200 Subject: [PATCH 2/2] ci: cargo fmt + regenerate API docs for #811 manifest entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cargo fmt --all and ./scripts/regen_api_docs.sh — fixes the lint and api-docs-drift checks on PR #818. --- crates/perry-api-manifest/src/entries.rs | 14 ++++++++++++-- crates/perry-ext-net/src/lib.rs | 9 ++++++--- docs/api/perry.d.ts | 16 +++++++++++++++- docs/src/api/reference.md | 9 ++++++++- 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/crates/perry-api-manifest/src/entries.rs b/crates/perry-api-manifest/src/entries.rs index fc81082f..c826eb4c 100644 --- a/crates/perry-api-manifest/src/entries.rs +++ b/crates/perry-api-manifest/src/entries.rs @@ -488,8 +488,18 @@ pub static API_MANIFEST: &[ApiEntry] = &[ method("net", "isIPv6", false, None), method("net", "getDefaultAutoSelectFamily", false, None), method("net", "setDefaultAutoSelectFamily", false, None), - method("net", "getDefaultAutoSelectFamilyAttemptTimeout", false, None), - method("net", "setDefaultAutoSelectFamilyAttemptTimeout", false, None), + method( + "net", + "getDefaultAutoSelectFamilyAttemptTimeout", + false, + None, + ), + method( + "net", + "setDefaultAutoSelectFamilyAttemptTimeout", + false, + None, + ), method_sig( "tls", "connect", diff --git a/crates/perry-ext-net/src/lib.rs b/crates/perry-ext-net/src/lib.rs index d955ca0f..6f9ba9ae 100644 --- a/crates/perry-ext-net/src/lib.rs +++ b/crates/perry-ext-net/src/lib.rs @@ -243,8 +243,7 @@ pub unsafe extern "C" fn js_net_is_ipv6(s_ptr: i64) -> f64 { // Happy-Eyeballs (auto-select-family) defaults. Process-wide globals // that `getDefault*` reads and `setDefault*` writes. -static AUTO_SELECT_FAMILY: std::sync::atomic::AtomicBool = - std::sync::atomic::AtomicBool::new(true); +static AUTO_SELECT_FAMILY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true); // Node's current default is 500ms (raised from 250 in v20.x); pin to // 500 so byte-for-byte parity holds against `node --experimental-strip-types`. static AUTO_SELECT_FAMILY_ATTEMPT_TIMEOUT_MS: std::sync::atomic::AtomicI32 = @@ -272,7 +271,11 @@ pub unsafe extern "C" fn js_net_get_default_auto_select_family_attempt_timeout() #[no_mangle] pub unsafe extern "C" fn js_net_set_default_auto_select_family_attempt_timeout(ms_f64: f64) -> f64 { let n = JsValue(ms_f64.to_bits()).to_number(); - let ms = if n.is_finite() && n >= 0.0 { n as i32 } else { 0 }; + let ms = if n.is_finite() && n >= 0.0 { + n as i32 + } else { + 0 + }; AUTO_SELECT_FAMILY_ATTEMPT_TIMEOUT_MS.store(ms, std::sync::atomic::Ordering::Relaxed); f64::from_bits(JsValue::UNDEFINED.0) } diff --git a/docs/api/perry.d.ts b/docs/api/perry.d.ts index d7902aad..1b090de9 100644 --- a/docs/api/perry.d.ts +++ b/docs/api/perry.d.ts @@ -1,6 +1,6 @@ // Auto-generated from Perry's API manifest (#465). Do not edit by hand. // Source: perry-api-manifest::API_MANIFEST -// Coverage: 826 entries across 70 modules +// Coverage: 833 entries across 70 modules declare module "argon2" { /** stdlib */ @@ -447,6 +447,20 @@ declare module "net" { export function connect(p0: any, p1: any, p2: any): any; /** stdlib */ export function createConnection(p0: any, p1: any, p2: any): any; + /** stdlib */ + export function getDefaultAutoSelectFamily(...args: any[]): any; + /** stdlib */ + export function getDefaultAutoSelectFamilyAttemptTimeout(...args: any[]): any; + /** stdlib */ + export function isIP(...args: any[]): any; + /** stdlib */ + export function isIPv4(...args: any[]): any; + /** stdlib */ + export function isIPv6(...args: any[]): any; + /** stdlib */ + export function setDefaultAutoSelectFamily(...args: any[]): any; + /** stdlib */ + export function setDefaultAutoSelectFamilyAttemptTimeout(...args: any[]): any; } declare module "node-cron" { diff --git a/docs/src/api/reference.md b/docs/src/api/reference.md index 72347527..8e2da61a 100644 --- a/docs/src/api/reference.md +++ b/docs/src/api/reference.md @@ -2,7 +2,7 @@ This page is auto-generated from Perry's compile-time API manifest (`perry-api-manifest::API_MANIFEST`). It is the source of truth for what `perry compile` accepts; references to symbols not listed here produce `R005 UnimplementedApi` (issue #463). Stubs (#464) are flagged ⚠ — they link cleanly but no-op at runtime on the chosen target. -Total: 826 entries across 70 modules. +Total: 833 entries across 70 modules. ## Modules @@ -704,7 +704,14 @@ Total: 826 entries across 70 modules. - `createConnection` — module - `destroy` — instance *(class: `Socket`)* - `end` — instance *(class: `Socket`)* +- `getDefaultAutoSelectFamily` — module +- `getDefaultAutoSelectFamilyAttemptTimeout` — module +- `isIP` — module +- `isIPv4` — module +- `isIPv6` — module - `on` — instance *(class: `Socket`)* +- `setDefaultAutoSelectFamily` — module +- `setDefaultAutoSelectFamilyAttemptTimeout` — module - `upgradeToTLS` — instance *(class: `Socket`)* - `write` — instance *(class: `Socket`)*