use ::std::cell::{Cell, RefCell};
use ::std::collections::HashMap;
use ::std::ptr::{self, NonNull};
use bun_core::ZBox;
use mozjs::conversions::unsafe_jsstr_to_string;
use mozjs::jsapi::*;
use mozjs::jsval::{
BooleanValue, DoubleValue, Int32Value, JSVal, NullValue, ObjectValue, StringValue,
UndefinedValue,
};
use mozjs::realm::AutoRealm;
use mozjs::rooted;
use mozjs::rust::wrappers2 as w2;
use bun_uws_sys::app::App;
use bun_uws_sys::socket_group::VTable;
use bun_uws_sys::{CloseCode, ListenSocket, Loop, SocketGroup, SocketKind, us_socket_t};
use crate::gc_store::{gc_store_get, gc_store_insert, gc_store_remove, gc_store_unique_key};
use crate::require::cache_builtin;
unsafe extern "C" {
fn inet_ntop(
af: ::std::ffi::c_int,
src: *const ::std::ffi::c_void,
dst: *mut ::std::ffi::c_char,
size: libc::socklen_t,
) -> *const ::std::ffi::c_char;
}
#[repr(C)]
#[allow(dead_code)]
struct NetSocketExt {
is_client: u8,
pending_write: NetPendingWrite,
}
#[repr(C)]
#[derive(Default)]
#[allow(dead_code)]
struct NetPendingWrite {
ptr: *mut u8,
len: usize,
cap: usize,
}
#[allow(dead_code)]
impl NetPendingWrite {
fn is_empty(&self) -> bool {
self.len == 0
}
fn set_data(&mut self, data: &[u8]) {
if data.is_empty() {
self.clear();
return;
}
let mut v = if self.cap > 0 && !self.ptr.is_null() {
unsafe { Vec::from_raw_parts(self.ptr, self.len, self.cap) }
} else {
Vec::new()
};
v.clear();
v.extend_from_slice(data);
let mut md = ::std::mem::ManuallyDrop::new(v);
self.ptr = md.as_mut_ptr();
self.len = md.len();
self.cap = md.capacity();
}
fn clear(&mut self) {
if self.cap > 0 && !self.ptr.is_null() {
unsafe {
drop(Vec::from_raw_parts(self.ptr, 0, self.cap));
}
}
self.ptr = ptr::null_mut();
self.len = 0;
self.cap = 0;
}
}
impl Drop for NetPendingWrite {
fn drop(&mut self) {
self.clear();
}
}
thread_local! {
static NET_SERVER_GROUPS: RefCell<HashMap<usize, Box<SocketGroup>>> = RefCell::new(HashMap::new());
static NET_LISTEN_SOCKETS: RefCell<Vec<usize>> = const { RefCell::new(Vec::new()) };
static NET_SOCKETS: RefCell<HashMap<usize, bool>> = RefCell::new(HashMap::new());
static CONNECT_RESULT: Cell<Option<usize>> = const { Cell::new(None) };
static CONNECT_ERROR: Cell<bool> = const { Cell::new(false) };
static NET_INCOMING_DATA: RefCell<HashMap<usize, Vec<u8>>> = RefCell::new(HashMap::new());
static NET_LISTEN_PORTS: RefCell<HashMap<usize, u16>> = RefCell::new(HashMap::new());
static NET_CONNECTION_CBS: RefCell<HashMap<usize, String>> = RefCell::new(HashMap::new());
static NET_GROUP_LISTEN: RefCell<HashMap<usize, usize>> = RefCell::new(HashMap::new());
static NET_EOF_SOCKETS: RefCell<HashMap<usize, bool>> = RefCell::new(HashMap::new());
static NET_CX: Cell<Option<*mut JSContext>> = const { Cell::new(None) };
}
pub struct NetCleanup;
impl Drop for NetCleanup {
fn drop(&mut self) {
NET_LISTEN_SOCKETS.with(|l| {
for key in l.borrow().iter() {
unsafe { crate::node_http::unregister_active_app(*key as *mut App<false>) };
}
});
NET_SERVER_GROUPS.with(|g| g.borrow_mut().clear());
NET_LISTEN_SOCKETS.with(|l| l.borrow_mut().clear());
NET_SOCKETS.with(|s| s.borrow_mut().clear());
NET_INCOMING_DATA.with(|d| d.borrow_mut().clear());
NET_LISTEN_PORTS.with(|p| p.borrow_mut().clear());
NET_CONNECTION_CBS.with(|c| c.borrow_mut().clear());
NET_GROUP_LISTEN.with(|m| m.borrow_mut().clear());
NET_EOF_SOCKETS.with(|e| e.borrow_mut().clear());
NET_CX.with(|c| c.set(None));
}
}
unsafe extern "C" fn net_on_open(
s: *mut us_socket_t,
is_client: ::std::ffi::c_int,
_ip: *mut u8,
_ip_length: ::std::ffi::c_int,
) -> *mut us_socket_t {
let key = s as usize;
NET_SOCKETS.with(|m| m.borrow_mut().insert(key, true));
if is_client != 0 {
CONNECT_RESULT.with(|r| {
if r.get().is_none() {
r.set(Some(key));
}
});
} else {
unsafe { dispatch_accept(s) };
}
s
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn dispatch_accept(s: *mut us_socket_t) {
let Some(cx) = NET_CX.with(|c| c.get()) else {
return;
};
if cx.is_null() {
return;
}
let group_ptr = (*s).group() as *mut SocketGroup as usize;
let listen_key = NET_GROUP_LISTEN.with(|m| m.borrow().get(&group_ptr).copied());
let Some(listen_key) = listen_key else { return };
let cb_key = NET_CONNECTION_CBS.with(|m| m.borrow().get(&listen_key).cloned());
let Some(cb_key) = cb_key else { return };
let Some(global) = bao_engine::context::thread_realm_global() else {
return;
};
if global.is_null() {
return;
}
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let global_root = global);
let mut realm = AutoRealm::new_from_handle(cx_ref, global_root.handle());
let realm_cx: &mut mozjs::context::JSContext = &mut realm;
let Some(handler) = gc_store_get(cx, &cb_key) else {
return;
};
if handler.is_null() {
return;
}
rooted!(&in(realm_cx) let ptr_arg = DoubleValue(s as usize as f64));
let factory_args = HandleValueArray {
length_: 1,
elements_: &*ptr_arg.handle(),
};
let mut sock_val = UndefinedValue();
let sock_ok = JS_CallFunctionName(
realm_cx.raw_cx(),
global_root.handle().into(),
c"__net_make_socket".as_ptr(),
&factory_args,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut sock_val,
},
);
if !sock_ok || !sock_val.is_object() {
JS_ClearPendingException(realm_cx.raw_cx());
return;
}
rooted!(&in(realm_cx) let sock_obj = sock_val.to_object());
let sock_h = sock_obj.handle().into();
let mut ip_buf = [0u8; 64];
if let Ok(ip) = (*s).remote_address(&mut ip_buf) {
let c_ip = ZBox::from_bytes(ip);
let ip_js = JS_NewStringCopyZ(realm_cx.raw_cx(), c_ip.as_ptr());
if !ip_js.is_null() {
rooted!(&in(realm_cx) let ip_v = StringValue(&*ip_js));
JS_DefineProperty(
realm_cx.raw_cx(),
sock_h,
c"remoteAddress".as_ptr(),
ip_v.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
rooted!(&in(realm_cx) let rp_v = Int32Value((*s).remote_port()));
JS_DefineProperty(
realm_cx.raw_cx(),
sock_h,
c"remotePort".as_ptr(),
rp_v.handle().into(),
JSPROP_ENUMERATE as u32,
);
rooted!(&in(realm_cx) let handler_val = ObjectValue(handler));
rooted!(&in(realm_cx) let sock_elem = ObjectValue(sock_obj.get()));
let call_args = HandleValueArray {
length_: 1,
elements_: &*sock_elem.handle(),
};
let mut rval = UndefinedValue();
let ok = JS_CallFunctionValue(
realm_cx.raw_cx(),
global_root.handle().into(),
handler_val.handle().into(),
&call_args,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut rval,
},
);
if !ok {
JS_ClearPendingException(realm_cx.raw_cx());
}
}
unsafe extern "C" fn net_on_data(
s: *mut us_socket_t,
data: *mut u8,
length: ::std::ffi::c_int,
) -> *mut us_socket_t {
let key = s as usize;
if length > 0 && !data.is_null() {
let slice = ::std::slice::from_raw_parts(data, length as usize);
NET_INCOMING_DATA.with(|m| {
let mut map = m.borrow_mut();
let buf = map.entry(key).or_insert_with(Vec::new);
buf.extend_from_slice(slice);
});
}
s
}
unsafe extern "C" fn net_on_writable(s: *mut us_socket_t) -> *mut us_socket_t {
s
}
unsafe extern "C" fn net_on_close(
s: *mut us_socket_t,
_code: ::std::ffi::c_int,
_reason: *mut ::std::ffi::c_void,
) -> *mut us_socket_t {
let key = s as usize;
NET_SOCKETS.with(|m| m.borrow_mut().remove(&key));
NET_INCOMING_DATA.with(|d| d.borrow_mut().remove(&key));
NET_EOF_SOCKETS.with(|e| e.borrow_mut().remove(&key));
s
}
unsafe extern "C" fn net_on_timeout(s: *mut us_socket_t) -> *mut us_socket_t {
s
}
unsafe extern "C" fn net_on_long_timeout(s: *mut us_socket_t) -> *mut us_socket_t {
s
}
unsafe extern "C" fn net_on_end(s: *mut us_socket_t) -> *mut us_socket_t {
let key = s as usize;
NET_EOF_SOCKETS.with(|e| e.borrow_mut().insert(key, true));
s
}
unsafe extern "C" fn net_on_connect_error(
s: *mut us_socket_t,
_code: ::std::ffi::c_int,
) -> *mut us_socket_t {
CONNECT_ERROR.with(|e| e.set(true));
CONNECT_RESULT.with(|r| r.set(Some(0))); unsafe {
(*s).close(CloseCode::failure);
}
s
}
unsafe extern "C" fn net_on_connecting_error(
_c: *mut bun_uws_sys::ConnectingSocket,
_code: ::std::ffi::c_int,
) -> *mut bun_uws_sys::ConnectingSocket {
CONNECT_ERROR.with(|e| e.set(true));
CONNECT_RESULT.with(|r| r.set(Some(0)));
_c
}
unsafe extern "C" fn net_on_handshake(
_s: *mut us_socket_t,
_success: ::std::ffi::c_int,
_err: bun_uws_sys::us_bun_verify_error_t,
_custom_data: *mut ::std::ffi::c_void,
) {
}
static NET_VTABLE: VTable = VTable {
on_open: Some(net_on_open),
on_data: Some(net_on_data),
on_fd: None,
on_writable: Some(net_on_writable),
on_close: Some(net_on_close),
on_timeout: Some(net_on_timeout),
on_long_timeout: Some(net_on_long_timeout),
on_end: Some(net_on_end),
on_connect_error: Some(net_on_connect_error),
on_connecting_error: Some(net_on_connecting_error),
on_handshake: Some(net_on_handshake),
};
const NET_JS: &str = r#"
(function() {
var EE = null;
try { EE = require("events").EventEmitter; } catch(e) {
EE = function EE() { this._events = {}; };
EE.prototype.on = function(e, fn) { (this._events[e] || (this._events[e] = [])).push(fn); return this; };
EE.prototype.emit = function(e) { var a = Array.prototype.slice.call(arguments, 1); var ls = this._events[e]; if (ls) for (var i = 0; i < ls.length; i++) ls[i].apply(this, a); return !!ls; };
EE.prototype.removeListener = function(e, fn) { var ls = this._events[e]; if (ls) { var i = ls.indexOf(fn); if (i >= 0) ls.splice(i, 1); } return this; };
}
// node:stream's pipe implementation, SHARED with node:stream (single
// owner — no parallel copy here). net.Socket extends stream.Duplex in
// Node, so socket.pipe(dest) is the canonical TCP→stream bridge; the
// socket satisfies the readable-source contract Readable.pipe needs
// (on/emit from EE, pause/resume below, _readableState.endEmitted set
// when 'end' is delivered). install_order guarantees the stream module
// is cached before node:net (globals::install_all), and the require
// global exists before any module install — a failure here is a broken
// runtime and fails loud instead of degrading to a local pipe copy.
var StreamPipe = require("stream").Readable.prototype.pipe;
function Socket(opts) {
EE.call(this);
this.destroyed = false;
this.connecting = false;
this._ptr = 0;
this._polling = false;
this._sawEnd = false;
this._paused = false;
// Minimal readable-state Readable.prototype.pipe reads (endEmitted for
// the late-attach case: pipe() after 'end' already delivered ends dest
// immediately).
this._readableState = { endEmitted: false };
}
Socket.prototype = Object.create(EE.prototype);
Socket.prototype.constructor = Socket;
Socket.prototype.connect = function(a0, a1, a2) {
// Node normalizeArgs semantics (net.html#socketconnectoptions-connectlistener):
// connect(options[, connectListener]) | connect(port[, host][, connectListener])
// The options form MUST be unwrapped here — handing the raw object to
// __net_connect made the native read a heap pointer as the port payload.
var port, host, cb;
if (a0 && typeof a0 === "object") {
port = a0.port;
host = a0.host || a0.hostname;
cb = a1;
} else {
port = a0;
if (typeof a1 === "function") { cb = a1; }
else { host = a1; cb = a2; }
}
if (typeof host === "function") { cb = host; host = "127.0.0.1"; }
if (!host) host = "127.0.0.1";
// Node accepts numeric strings for port ("8080"); coerce so the native
// always receives a Number. Malformed values fall through and the native
// throws (NaN / out-of-range are rejected there with a Node-style message).
if (typeof port === "string") port = Number(port);
this.connecting = true;
if (typeof __net_connect === "function") {
var ptr = __net_connect(port, host);
if (ptr > 0) {
this._ptr = ptr;
this.connecting = false;
// Node semantics: the 'connect' event and the connect callback fire
// on a LATER tick — never synchronously inside net.connect(). The
// synchronous form broke `var c = net.connect(p, h, function () {
// c.on(...) })` and `c.on('connect')` registered after the call: the
// callback ran while `c` was still undefined. Scheduling BEFORE
// _startPoll keeps 'connect' ahead of any 'data' (both are
// setTimeout(0); same-deadline timers fire in registration order).
var self = this;
setTimeout(function () {
if (self.destroyed || self._ptr === 0) return;
self.emit("connect");
if (cb) cb();
}, 0);
this._startPoll();
} else {
// 'error' equally deferred: the listener is registered after
// net.connect() returns in the common `var c = net.connect(...);
// c.on('error', ...)` shape.
var self = this;
setTimeout(function () {
if (self.destroyed) return;
self.emit("error", new Error("connect ECONNREFUSED " + host + ":" + port));
}, 0);
}
}
return this;
};
Socket.prototype.write = function(data) {
if (this.destroyed || this._ptr === 0) return false;
if (typeof __net_write === "function") {
return __net_write(this._ptr, data) >= 0;
}
return false;
};
Socket.prototype.end = function(data) {
// Node semantics: end() is idempotent — a second end() (including the
// canonical `sock.on('end', () => sock.end())` half-close echo shape) is
// a no-op. Without the guard the re-entrant end() re-emitted 'end'
// synchronously, recursing until SpiderMonkey's "too much recursion"
// throw aborted the poll tick mid-delivery (flaky peer-FIN test, log
// flooded with hundreds of 'end' events per single FIN).
if (this.destroyed) return this;
if (data) this.write(data);
this.destroyed = true;
this._stopPoll();
if (typeof __net_close === "function") {
__net_close(this._ptr);
}
this._ptr = 0;
this._readableState.endEmitted = true;
this.emit("end");
this.emit("close");
return this;
};
Socket.prototype.on = function(event, listener) {
EE.prototype.on.call(this, event, listener);
if (event === "data" && this._ptr > 0) {
this._startPoll();
}
return this;
};
Socket.prototype.destroy = function() {
if (this.destroyed) return this;
this.destroyed = true;
this._stopPoll();
if (this._ptr > 0 && typeof __net_close === "function") {
__net_close(this._ptr);
}
this._ptr = 0;
this.emit("close");
return this;
};
// ── Duplex face: readable-side flow control + pipe ──
// pause() halts the poll chain (the first _pollTick guard early-returns
// on !_polling, so an in-flight scheduled tick is also dropped); native RX
// bytes buffer in NET_INCOMING_DATA and flow again on resume() — REAL
// backpressure for `dest.write(c) === false → src.pause()` from
// Readable.prototype.pipe, with dest's 'drain' resuming via resume().
Socket.prototype.pause = function() {
this._paused = true;
this._polling = false;
return this;
};
Socket.prototype.resume = function() {
if (!this._paused) return this;
this._paused = false;
if (!this._polling && !this.destroyed && this._ptr > 0) this._startPoll();
return this;
};
Socket.prototype.isPaused = function() { return !!this._paused; };
// node:stream's pipe (StreamPipe above): socket → writable dest, with
// data/end/error forwarding, 'pipe' notification, backpressure via
// pause/resume, and dest.end() on source end. The reverse direction —
// readable.pipe(socket) — works duck-typed on the writable face
// (write/end/on/emit), no wiring needed here.
Socket.prototype.pipe = StreamPipe;
// Poll __net_read for buffered incoming data and emit 'data' events
Socket.prototype._startPoll = function() {
if (this._polling || this._ptr === 0) return;
this._polling = true;
// DEFERRED first tick — same class as the CP shim fix: a synchronous
// first tick drained buffered data before listeners registered later in
// the same block (on('connect') cb writing, then on('data')) could see it.
setTimeout(this._pollTick.bind(this), 0);
};
Socket.prototype._stopPoll = function() {
this._polling = false;
};
Socket.prototype._pollTick = function() {
if (!this._polling || this.destroyed || this._ptr === 0) return;
// Socket lifecycle from the native side: 1 open, 2 peer-FIN seen,
// 3 fully closed. Without this the poll chain spun forever after the
// peer (or the server's close_all) closed the socket, holding the event
// loop open and never delivering 'end'/'close'. usockets commonly closes
// the socket right after dispatching on_end (no half-open window), so the
// poll may first observe state 3 — deliver 'end' before 'close' there
// too (Node ordering: end precedes close).
if (typeof __net_poll_state === "function") {
var st = __net_poll_state(this._ptr);
if (st === 3) {
this._stopPoll();
this._ptr = 0;
this.destroyed = true;
if (!this._sawEnd) {
this._sawEnd = true;
this._readableState.endEmitted = true;
this.emit("end");
}
this.emit("close");
return;
}
if (st === 2 && !this._sawEnd) {
this._sawEnd = true;
this._readableState.endEmitted = true;
this.emit("end");
}
}
if (typeof __net_read === "function") {
var buf = __net_read(this._ptr);
// __net_read returns an ArrayBuffer (transfer-owned) — length lives on
// .byteLength; the old `.length` check was always undefined and 'data'
// never fired.
// BCE-20260816-NET-DATABUFFER — Node delivers 'data' chunks as Buffer,
// not ArrayBuffer (audit: net chunk arrived as ArrayBuffer so
// Buffer.isBuffer(chunk) === false and .toString(enc) was missing).
// Buffer.view over the transferred ArrayBuffer (zero-copy).
if (buf && buf.byteLength > 0) {
this.emit("data", Buffer.from(buf));
}
}
// Schedule next poll via setTimeout(0) to yield to other events
if (this._polling && !this.destroyed && this._ptr !== 0) {
setTimeout(this._pollTick.bind(this), 0);
}
};
function Server(opts, connectionListener) {
if (typeof opts === "function") { connectionListener = opts; opts = null; }
EE.call(this);
this.listening = false;
this._ptr = 0;
this._port = 0;
if (connectionListener) this.on("connection", connectionListener);
}
Server.prototype = Object.create(EE.prototype);
Server.prototype.constructor = Server;
Server.prototype.listen = function() {
var port = 0, host = "0.0.0.0", cb;
for (var i = 0; i < arguments.length; i++) {
var arg = arguments[i];
if (typeof arg === "function") cb = arg;
else if (arg && typeof arg === "object") {
// Node options form: server.listen({ port, host }, [cb]). The object
// matched none of the typeof branches before, so listen({port: N})
// silently bound a RANDOM port (0) instead of N.
if (typeof arg.port === "number") port = arg.port;
else if (typeof arg.port === "string") port = Number(arg.port);
if (typeof arg.host === "string") host = arg.host;
else if (typeof arg.hostname === "string") host = arg.hostname;
}
else if (typeof arg === "number") port = arg;
else if (typeof arg === "string") host = arg;
}
if (typeof __net_listen === "function") {
var ptr = __net_listen(port, host);
if (ptr > 0) {
this._ptr = ptr;
this._port = port;
this.listening = true;
// Register the accept dispatcher BEFORE any callback runs: the native
// side (dispatch_accept, vtable on_open with is_client == 0) drops the
// accept silently when NET_CONNECTION_CBS has no entry for the listen
// socket. A 'listening' callback that immediately net.connect()s to
// itself spins the loop inline (net_connect waits for the real TCP
// open), so the inbound accept can dispatch inside that spin — before
// this function returns. Registering after `cb()` (the old order)
// lost that first connection forever (echo server never saw it).
if (typeof __net_on_connection === "function") {
var self = this;
__net_on_connection(ptr, function(sock) {
self.emit("connection", sock);
});
}
this.emit("listening");
if (cb) cb();
} else {
this.emit("error", new Error("listen EADDRINUSE"));
}
}
return this;
};
Server.prototype.close = function(cb) {
this.listening = false;
if (this._ptr > 0 && typeof __net_close === "function") {
__net_close(this._ptr);
}
this._ptr = 0;
this.emit("close");
if (cb) cb();
return this;
};
Server.prototype.address = function() {
if (this._ptr > 0 && typeof __net_address === "function") {
var addr = __net_address(this._ptr);
if (addr) return addr;
}
// Fallback: return the port passed to listen() if getsockname failed
if (this._port > 0) {
return { port: this._port, family: "IPv4", address: "0.0.0.0" };
}
return { port: 0, family: "IPv4", address: "0.0.0.0" };
};
function isIP(input) {
if (!input || typeof input !== "string") return 0;
// Check IPv4
var parts = input.split(".");
if (parts.length === 4) {
for (var i = 0; i < 4; i++) {
var n = parseInt(parts[i], 10);
if (isNaN(n) || n < 0 || n > 255 || parts[i] !== String(n)) return 0;
}
return 4;
}
// Check IPv6 — use native __net_isIPv6 if available for robust detection
if (typeof __net_isIPv6 === "function") {
if (__net_isIPv6(input)) return 6;
} else if (input.indexOf(":") !== -1) {
return isIPv6String(input) ? 6 : 0;
}
return 0;
}
function isIPv6String(input) {
// Basic IPv6 validation: must contain ':', valid hextets and '::' compression
if (input.indexOf(":") === -1) return false;
// Reject embedded IPv4 unless it's the last two parts (::ffff:1.2.3.4)
var doubleColon = input.indexOf("::");
if (doubleColon !== input.lastIndexOf("::")) return false; // only one :: allowed
var segments = input.split(":");
// Handle trailing IPv4 mapped address (e.g. ::ffff:192.168.1.1)
var lastSeg = segments[segments.length - 1];
if (lastSeg && lastSeg.indexOf(".") !== -1) {
var v4parts = lastSeg.split(".");
if (v4parts.length !== 4) return false;
for (var j = 0; j < 4; j++) {
var n = parseInt(v4parts[j], 10);
if (isNaN(n) || n < 0 || n > 255 || v4parts[j] !== String(n)) return false;
}
segments = segments.slice(0, -1);
}
if (segments.length > 8) return false;
var hasDoubleColon = input.indexOf("::") !== -1;
if (!hasDoubleColon && segments.length !== 8) return false;
if (hasDoubleColon && segments.length >= 8) return false;
for (var i = 0; i < segments.length; i++) {
var seg = segments[i];
if (seg === "" && (i === 0 || i === segments.length - 1)) continue; // leading/trailing empty from ::
if (seg === "") continue; // empty from :: expansion
if (!/^[0-9a-fA-F]{1,4}$/.test(seg)) return false;
}
return true;
}
// Accept-bridge factory: the native side (dispatch_accept) calls this to
// build the JS net.Socket for a usockets-accepted socket — the prototype
// chain and the __net_read poll machinery stay owned by this IIFE. Written
// TO the global (not probed FROM it), so the free-variable probe class
// fixed in bbe20a81 does not apply.
try {
globalThis.__net_make_socket = function(ptr) {
var s = new Socket();
s._ptr = ptr;
s.connecting = false;
return s;
};
} catch (e) { /* globalThis unavailable — accept bridge disabled */ }
return {
Socket: Socket,
Server: Server,
createServer: function(opts, cb) { return new Server(opts, cb); },
connect: function(a0, a1, a2) { var s = new Socket(); return s.connect(a0, a1, a2); },
createConnection: function(a0, a1, a2) { var s = new Socket(); return s.connect(a0, a1, a2); },
isIP: isIP,
isIPv4: function(input) { return isIP(input) === 4; },
isIPv6: function(input) { return isIP(input) === 6; },
};
})();
"#;
#[inline]
fn jsval_to_ptr(val: &JSVal) -> usize {
if val.is_double() {
val.to_double() as usize
} else if val.is_int32() {
val.to_int32() as usize
} else {
0
}
}
#[inline]
fn ptr_to_jsval(ptr: usize) -> JSVal {
DoubleValue(ptr as f64)
}
fn get_loop() -> *mut Loop {
bao_uloop::force_link();
bao_uloop::uws_get_loop()
}
fn ensure_server_group(loop_: *mut Loop) -> *mut SocketGroup {
let mut group = Box::new(SocketGroup::default());
group.init(loop_, Some(&NET_VTABLE), ptr::null_mut());
Box::into_raw(group)
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn net_listen(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let port: i32 = if argc > 0 {
let port_val = *args.get(0).ptr;
if !port_val.is_number() {
JS_ReportErrorUTF8(
cx,
c"The 'port' argument must be a number. Received a non-number value."
.as_ptr(),
);
return false;
}
let d = port_val.to_number();
if !d.is_finite() || d < 0.0 || d > 65535.0 {
let msg = format!("Port should be >= 0 and < 65536. Received {}.", d);
let c_msg = ZBox::from_bytes(msg.as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), (*c_msg).as_ptr());
return false;
}
d as i32
} else {
0
};
let addr = if argc > 1 && (*args.get(1).ptr).is_string() {
unsafe_jsstr_to_string(cx, NonNull::new_unchecked((*args.get(1).ptr).to_string()))
} else {
"0.0.0.0".to_string()
};
let loop_ = get_loop();
if loop_.is_null() {
args.rval().set(Int32Value(0));
return true;
}
let group_ptr = ensure_server_group(loop_);
let group: &mut SocketGroup = unsafe { &mut *group_ptr };
let host_cstr = ZBox::from_bytes(addr.as_bytes());
let mut err: ::std::ffi::c_int = 0;
let listen_socket = group.listen(
SocketKind::UwsHttp, None, Some((*host_cstr).as_cstr()),
port,
0, 0, &mut err,
);
let _ = err;
if listen_socket.is_null() {
unsafe {
SocketGroup::destroy(group_ptr);
}
args.rval().set(Int32Value(0));
return true;
}
let listen_key = listen_socket as usize;
NET_SERVER_GROUPS.with(|g| {
g.borrow_mut()
.insert(listen_key, unsafe { Box::from_raw(group_ptr) })
});
NET_LISTEN_SOCKETS.with(|l| l.borrow_mut().push(listen_key));
NET_GROUP_LISTEN.with(|m| m.borrow_mut().insert(group_ptr as usize, listen_key));
unsafe { crate::node_http::register_active_app(listen_socket as *mut App<false>) };
NET_LISTEN_PORTS.with(|p| p.borrow_mut().insert(listen_key, port as u16));
args.rval().set(ptr_to_jsval(listen_key));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn net_connect(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let port: i32 = if argc > 0 {
let port_val = *args.get(0).ptr;
if !port_val.is_number() {
JS_ReportErrorUTF8(
cx,
c"The 'port' argument must be a number. Received a non-number value."
.as_ptr(),
);
return false;
}
let d = port_val.to_number();
if !d.is_finite() || d < 0.0 || d > 65535.0 {
let msg = format!("Port should be >= 0 and < 65536. Received {}.", d);
let c_msg = ZBox::from_bytes(msg.as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), (*c_msg).as_ptr());
return false;
}
d as i32
} else {
0
};
let addr = if argc > 1 && (*args.get(1).ptr).is_string() {
unsafe_jsstr_to_string(cx, NonNull::new_unchecked((*args.get(1).ptr).to_string()))
} else {
"127.0.0.1".to_string()
};
let loop_ = get_loop();
if loop_.is_null() {
args.rval().set(Int32Value(0));
return true;
}
let mut group = Box::new(SocketGroup::default());
group.init(loop_, Some(&NET_VTABLE), ptr::null_mut());
let group_ptr = Box::into_raw(group);
let host_cstr = ZBox::from_bytes(addr.as_bytes());
CONNECT_RESULT.with(|r| r.set(None));
CONNECT_ERROR.with(|e| e.set(false));
let result = (*group_ptr).connect(
SocketKind::UwsHttp,
None,
(*host_cstr).as_cstr(),
port,
0,
0, );
match result {
bun_uws_sys::ConnectResult::Socket(socket) => {
let key = socket as usize;
NET_SERVER_GROUPS.with(|g| {
g.borrow_mut()
.insert(key, unsafe { Box::from_raw(group_ptr) })
});
let max_ticks: u32 = 5000;
for _ in 0..max_ticks {
let done = CONNECT_RESULT.with(|r| r.get().is_some());
if done {
break;
}
unsafe {
bao_uloop::bao_loop_tick(loop_, ptr::null());
}
}
let error = CONNECT_ERROR.with(|e| e.get());
let result_key = CONNECT_RESULT.with(|r| r.get().unwrap_or(0));
if error || result_key == 0 {
if let Some(group_box) = NET_SERVER_GROUPS.with(|g| g.borrow_mut().remove(&key)) {
let raw = Box::into_raw(group_box);
unsafe {
SocketGroup::destroy(raw);
drop(Box::from_raw(raw));
}
}
args.rval().set(Int32Value(0));
} else {
NET_SOCKETS.with(|m| m.borrow_mut().insert(result_key, true));
args.rval().set(ptr_to_jsval(result_key));
}
}
bun_uws_sys::ConnectResult::Connecting(_connecting) => {
let group_key = group_ptr as usize;
NET_SERVER_GROUPS.with(|g| {
g.borrow_mut()
.insert(group_key, unsafe { Box::from_raw(group_ptr) })
});
let max_ticks: u32 = 5000;
for _ in 0..max_ticks {
let done = CONNECT_RESULT.with(|r| r.get().is_some());
if done {
break;
}
unsafe {
bao_uloop::bao_loop_tick(loop_, ptr::null());
}
}
let error = CONNECT_ERROR.with(|e| e.get());
let result_key = CONNECT_RESULT.with(|r| r.get().unwrap_or(0));
if error || result_key == 0 {
args.rval().set(Int32Value(0));
} else {
NET_SOCKETS.with(|m| m.borrow_mut().insert(result_key, true));
args.rval().set(ptr_to_jsval(result_key));
}
}
bun_uws_sys::ConnectResult::Failed => {
unsafe {
SocketGroup::destroy(group_ptr);
}
args.rval().set(Int32Value(0));
}
}
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn net_write(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc < 2 {
args.rval().set(Int32Value(-1));
return true;
}
let ptr_val = jsval_to_ptr(&(*args.get(0).ptr));
let data: Vec<u8> = if (*args.get(1).ptr).is_string() {
unsafe_jsstr_to_string(cx, NonNull::new_unchecked((*args.get(1).ptr).to_string()))
.into_bytes()
} else {
match crate::node_buffer::collect_byte_view(cx, *args.get(1).ptr) {
Some(b) => b,
None => {
args.rval().set(Int32Value(-1));
return true;
}
}
};
let socket_ptr = ptr_val as *mut us_socket_t;
let exists = NET_SOCKETS.with(|m| m.borrow().contains_key(&ptr_val));
if !exists {
args.rval().set(Int32Value(-1));
return true;
}
let written = unsafe { (*socket_ptr).write(&data) };
args.rval().set(Int32Value(written));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn net_close(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let ptr_val = if argc > 0 {
jsval_to_ptr(&(*args.get(0).ptr))
} else {
0
};
if ptr_val == 0 {
args.rval().set(UndefinedValue());
return true;
}
let was_socket = NET_SOCKETS.with(|m| m.borrow_mut().remove(&ptr_val).is_some());
if was_socket {
let socket_ptr = ptr_val as *mut us_socket_t;
unsafe {
(*socket_ptr).close(CloseCode::normal);
}
}
let is_listen = NET_LISTEN_SOCKETS.with(|l| {
let mut list = l.borrow_mut();
match list.iter().position(|&k| k == ptr_val) {
Some(pos) => {
list.swap_remove(pos);
true
}
None => false,
}
});
if is_listen {
if let Some(group_box) = NET_SERVER_GROUPS.with(|g| g.borrow_mut().remove(&ptr_val)) {
let raw = Box::into_raw(group_box);
let group_addr = raw as usize;
unsafe {
(*raw).close_all();
SocketGroup::destroy(raw);
drop(Box::from_raw(raw));
}
NET_GROUP_LISTEN.with(|m| m.borrow_mut().remove(&group_addr));
} else {
let listen_ptr = ptr_val as *mut ListenSocket;
unsafe {
(*listen_ptr).close();
}
}
NET_LISTEN_PORTS.with(|p| p.borrow_mut().remove(&ptr_val));
NET_CONNECTION_CBS.with(|c| {
if let Some(key) = c.borrow_mut().remove(&ptr_val) {
gc_store_remove(cx, &key);
}
});
unsafe { crate::node_http::unregister_active_app(ptr_val as *mut App<false>) };
} else {
NET_SERVER_GROUPS.with(|g| g.borrow_mut().remove(&ptr_val));
}
args.rval().set(UndefinedValue());
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn net_address(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let ptr_val = if argc > 0 {
jsval_to_ptr(&(*args.get(0).ptr))
} else {
0
};
if ptr_val == 0 {
args.rval().set(ObjectValue(::std::ptr::null_mut()));
return true;
}
let listen_ptr = ptr_val as *mut ListenSocket;
let port = unsafe { (*listen_ptr).get_local_port() };
let mut addr: libc::sockaddr_storage = unsafe { ::std::mem::zeroed() };
let mut addr_len: libc::socklen_t =
::std::mem::size_of::<libc::sockaddr_storage>() as libc::socklen_t;
let fd = unsafe { (*listen_ptr).fd() };
let (address_str, family_str, resolved_port) = if unsafe {
libc::getsockname(
fd.native(),
&mut addr as *mut _ as *mut libc::sockaddr,
&mut addr_len,
)
} == 0
{
let actual_port = if addr.ss_family as i32 == libc::AF_INET6 {
let addr_in6 = &addr as *const _ as *const libc::sockaddr_in6;
unsafe { u16::from_be((*addr_in6).sin6_port) as i32 }
} else {
let addr_in = &addr as *const _ as *const libc::sockaddr_in;
unsafe { u16::from_be((*addr_in).sin_port) as i32 }
};
if addr.ss_family as i32 == libc::AF_INET6 {
let addr_in6 = &addr as *const _ as *const libc::sockaddr_in6;
let mut buf = [0u8; 64];
let ok = unsafe {
inet_ntop(
libc::AF_INET6,
&(*addr_in6).sin6_addr as *const _ as *const ::std::ffi::c_void,
buf.as_mut_ptr() as *mut ::std::ffi::c_char,
buf.len() as libc::socklen_t,
)
};
let addr_str = if ok.is_null() {
"::".to_string()
} else {
unsafe { ::std::ffi::CStr::from_ptr(ok) }
.to_string_lossy()
.into_owned()
};
(addr_str, "IPv6", actual_port)
} else {
let addr_in = &addr as *const _ as *const libc::sockaddr_in;
let mut buf = [0u8; 32];
let ok = unsafe {
inet_ntop(
libc::AF_INET,
&(*addr_in).sin_addr as *const _ as *const ::std::ffi::c_void,
buf.as_mut_ptr() as *mut ::std::ffi::c_char,
buf.len() as libc::socklen_t,
)
};
let addr_str = if ok.is_null() {
"0.0.0.0".to_string()
} else {
unsafe { ::std::ffi::CStr::from_ptr(ok) }
.to_string_lossy()
.into_owned()
};
(addr_str, "IPv4", actual_port)
}
} else {
let fallback_port = if port > 0 {
port
} else {
NET_LISTEN_PORTS.with(|p| p.borrow().get(&ptr_val).copied().unwrap_or(0) as i32)
};
("0.0.0.0".to_string(), "IPv4", fallback_port)
};
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let result_obj = w2::JS_NewPlainObject(cx_ref));
if result_obj.get().is_null() {
args.rval().set(UndefinedValue());
return true;
}
let result_h = result_obj.handle().into();
rooted!(&in(cx_ref) let pv = Int32Value(resolved_port));
JS_DefineProperty(
cx,
result_h,
c"port".as_ptr(),
pv.handle().into(),
JSPROP_ENUMERATE as u32,
);
let c_family = ZBox::from_bytes(family_str.as_bytes());
let family_js = JS_NewStringCopyZ(cx, c_family.as_ptr());
if !family_js.is_null() {
rooted!(&in(cx_ref) let fv = StringValue(&*family_js));
JS_DefineProperty(
cx,
result_h,
c"family".as_ptr(),
fv.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
let c_addr = ZBox::from_bytes(address_str.as_bytes());
let addr_js = JS_NewStringCopyZ(cx, c_addr.as_ptr());
if !addr_js.is_null() {
rooted!(&in(cx_ref) let av = StringValue(&*addr_js));
JS_DefineProperty(
cx,
result_h,
c"address".as_ptr(),
av.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
args.rval().set(ObjectValue(result_obj.get()));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn net_read(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let ptr_val = if argc > 0 {
jsval_to_ptr(&(*args.get(0).ptr))
} else {
0
};
if ptr_val == 0 {
args.rval().set(NullValue());
return true;
}
let data = NET_INCOMING_DATA.with(|m| {
let mut map = m.borrow_mut();
match map.remove(&ptr_val) {
Some(v) => v,
None => Vec::new(),
}
});
if data.is_empty() {
args.rval().set(NullValue());
return true;
}
let len = data.len();
let buf_ptr = data.as_ptr();
let alloc = ::std::alloc::alloc(
::std::alloc::Layout::from_size_align(len, 1)
.unwrap_or_else(|_| ::std::alloc::Layout::from_size_align(1, 1).unwrap()),
);
if alloc.is_null() {
args.rval().set(NullValue());
return true;
}
unsafe {
::std::ptr::copy_nonoverlapping(buf_ptr, alloc, len);
}
let mut wrapped_cx = unsafe { mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx)) };
let cx_ref = &mut wrapped_cx;
let array_buffer =
w2::NewArrayBufferWithContents(cx_ref, len, alloc as *mut ::std::os::raw::c_void);
if array_buffer.is_null() {
::std::alloc::dealloc(
alloc,
::std::alloc::Layout::from_size_align(len, 1)
.unwrap_or_else(|_| ::std::alloc::Layout::from_size_align(1, 1).unwrap()),
);
args.rval().set(NullValue());
return true;
}
rooted!(&in(cx_ref) let ab = array_buffer);
args.rval().set(ObjectValue(ab.get()));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn net_is_ipv6(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 || !(*args.get(0).ptr).is_string() {
args.rval().set(BooleanValue(false));
return true;
}
let input = unsafe_jsstr_to_string(cx, NonNull::new_unchecked((*args.get(0).ptr).to_string()));
let result = input.contains(':');
args.rval().set(BooleanValue(result));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn net_on_connection(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc < 2 {
args.rval().set(BooleanValue(false));
return true;
}
let listen_ptr = jsval_to_ptr(&(*args.get(0).ptr));
let cb_val = *args.get(1).ptr;
if listen_ptr == 0 || !cb_val.is_object() {
args.rval().set(BooleanValue(false));
return true;
}
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let cb_obj = cb_val.to_object());
if !unsafe { JS_ObjectIsFunction(cb_obj.get()) } {
args.rval().set(BooleanValue(false));
return true;
}
let key = gc_store_unique_key(&format!("net_connection_{}", listen_ptr));
gc_store_insert(cx, &key, cb_obj.get());
NET_CONNECTION_CBS.with(|c| c.borrow_mut().insert(listen_ptr, key));
args.rval().set(BooleanValue(true));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn net_poll_state(_cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let ptr_val = if argc > 0 {
jsval_to_ptr(&(*args.get(0).ptr))
} else {
0
};
if ptr_val == 0 {
args.rval().set(Int32Value(3));
return true;
}
let open = NET_SOCKETS.with(|m| m.borrow().contains_key(&ptr_val));
if !open {
args.rval().set(Int32Value(3));
return true;
}
let eof = NET_EOF_SOCKETS.with(|e| e.borrow().contains_key(&ptr_val));
args.rval().set(Int32Value(if eof { 2 } else { 1 }));
true
}
pub fn install(cx: &mut mozjs::context::JSContext) {
rooted!(&in(cx) let mod_obj = unsafe { w2::JS_NewPlainObject(cx) });
if mod_obj.get().is_null() {
return;
}
unsafe {
let cx_raw = cx.raw_cx();
JS_DefineFunction(
cx_raw,
mod_obj.handle().into(),
c"__net_listen".as_ptr(),
Some(net_listen),
2,
0,
);
JS_DefineFunction(
cx_raw,
mod_obj.handle().into(),
c"__net_connect".as_ptr(),
Some(net_connect),
2,
0,
);
JS_DefineFunction(
cx_raw,
mod_obj.handle().into(),
c"__net_write".as_ptr(),
Some(net_write),
2,
0,
);
JS_DefineFunction(
cx_raw,
mod_obj.handle().into(),
c"__net_close".as_ptr(),
Some(net_close),
1,
0,
);
JS_DefineFunction(
cx_raw,
mod_obj.handle().into(),
c"__net_address".as_ptr(),
Some(net_address),
1,
0,
);
JS_DefineFunction(
cx_raw,
mod_obj.handle().into(),
c"__net_read".as_ptr(),
Some(net_read),
1,
0,
);
JS_DefineFunction(
cx_raw,
mod_obj.handle().into(),
c"__net_isIPv6".as_ptr(),
Some(net_is_ipv6),
1,
0,
);
JS_DefineFunction(
cx_raw,
mod_obj.handle().into(),
c"__net_on_connection".as_ptr(),
Some(net_on_connection),
2,
0,
);
JS_DefineFunction(
cx_raw,
mod_obj.handle().into(),
c"__net_poll_state".as_ptr(),
Some(net_poll_state),
1,
0,
);
let global = CurrentGlobalOrNull(cx_raw);
if !global.is_null() {
rooted!(&in(cx) let global_root = global);
let bridges: &[(&str, JSNative, u32)] = &[
("__net_listen", Some(net_listen), 2),
("__net_connect", Some(net_connect), 2),
("__net_write", Some(net_write), 2),
("__net_close", Some(net_close), 1),
("__net_address", Some(net_address), 1),
("__net_read", Some(net_read), 1),
("__net_isIPv6", Some(net_is_ipv6), 1),
("__net_on_connection", Some(net_on_connection), 2),
("__net_poll_state", Some(net_poll_state), 1),
];
for &(name, native, nargs) in bridges {
let c_name = ZBox::from_bytes(name);
JS_DefineFunction(
cx_raw,
global_root.handle().into(),
c_name.as_ptr(),
native,
nargs,
0,
);
}
}
NET_CX.with(|c| c.set(Some(cx_raw)));
let c_filename = ZBox::from_bytes("node:net".as_bytes());
let opts = mozjs::glue::NewCompileOptions(cx_raw, c_filename.as_ptr(), 1);
if opts.is_null() {
return;
}
let mut src = mozjs::rust::transform_str_to_source_text(NET_JS);
let mut rval = UndefinedValue();
let rval_handle = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut rval,
};
let ok = mozjs_sys::jsapi::JS::Evaluate2(cx_raw, opts, &mut src, rval_handle);
libc::free(opts as *mut _);
if !ok || !rval.is_object() {
return;
}
let exports_obj = rval.to_object();
rooted!(&in(cx) let exports_rooted = exports_obj);
for name in &[
"Socket",
"Server",
"createServer",
"connect",
"createConnection",
"isIP",
"isIPv4",
"isIPv6",
] {
let cname = ZBox::from_bytes(name.as_bytes());
let mut val = UndefinedValue();
JS_GetProperty(
cx_raw,
exports_rooted.handle().into(),
cname.as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut val,
},
);
if !val.is_undefined() {
rooted!(&in(cx) let val_root = val);
JS_DefineProperty(
cx_raw,
mod_obj.handle().into(),
cname.as_ptr(),
val_root.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
cache_builtin(cx, "net", mod_obj.get());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_net_vtable_is_complete() {
assert!(NET_VTABLE.on_open.is_some(), "on_open must be set");
assert!(NET_VTABLE.on_data.is_some(), "on_data must be set");
assert!(NET_VTABLE.on_close.is_some(), "on_close must be set");
assert!(NET_VTABLE.on_writable.is_some(), "on_writable must be set");
assert!(NET_VTABLE.on_end.is_some(), "on_end must be set");
assert!(NET_VTABLE.on_timeout.is_some(), "on_timeout must be set");
assert!(
NET_VTABLE.on_connect_error.is_some(),
"on_connect_error must be set"
);
assert!(
NET_VTABLE.on_connecting_error.is_some(),
"on_connecting_error must be set"
);
assert!(
NET_VTABLE.on_handshake.is_some(),
"on_handshake must be set"
);
}
#[test]
fn test_get_loop_returns_non_null() {
bao_uloop::force_link();
let loop_ = get_loop();
assert!(
!loop_.is_null(),
"get_loop must return non-null after force_link"
);
}
#[test]
fn test_net_pending_write_empty() {
let pw = NetPendingWrite::default();
assert!(pw.is_empty());
assert_eq!(pw.len, 0);
}
#[test]
fn test_net_pending_write_set_and_clear() {
let mut pw = NetPendingWrite::default();
pw.set_data(b"hello");
assert!(!pw.is_empty());
assert_eq!(pw.len, 5);
pw.clear();
assert!(pw.is_empty());
}
#[test]
fn test_net_pending_write_set_empty_data() {
let mut pw = NetPendingWrite::default();
pw.set_data(b"first");
pw.set_data(b"");
assert!(pw.is_empty());
}
#[test]
fn test_net_pending_write_overwrite() {
let mut pw = NetPendingWrite::default();
pw.set_data(b"hello");
pw.set_data(b"world!");
assert_eq!(pw.len, 6);
pw.clear();
}
#[test]
fn test_net_cleanup_does_not_panic() {
let _cleanup = NetCleanup;
}
#[test]
fn test_socket_kind_tcp() {
let kind = SocketKind::UwsHttp;
assert_ne!(kind, SocketKind::Invalid);
}
#[test]
fn test_close_code_normal() {
assert_eq!(CloseCode::normal as i32, 0);
}
#[test]
fn test_js_source_contains_ptr_not_fd() {
assert!(
NET_JS.contains("_ptr"),
"JS must use _ptr for socket reference"
);
assert!(!NET_JS.contains("_fd"), "JS must not use _fd");
}
#[test]
fn test_js_source_contains_all_exports() {
for name in &[
"Socket",
"Server",
"createServer",
"connect",
"createConnection",
"isIP",
"isIPv4",
"isIPv6",
] {
assert!(NET_JS.contains(name), "JS must export {}", name);
}
}
#[test]
fn test_net_socket_ext_layout() {
assert!(::std::mem::size_of::<NetSocketExt>() > 0);
assert!(::std::mem::size_of::<NetSocketExt>() >= ::std::mem::size_of::<u8>());
}
#[test]
fn test_ensure_server_group_creates_valid_group() {
bao_uloop::force_link();
let loop_ = get_loop();
assert!(!loop_.is_null());
let group_ptr = ensure_server_group(loop_);
assert!(!group_ptr.is_null());
unsafe {
SocketGroup::destroy(group_ptr);
}
}
#[test]
fn test_net_vtable_callback_signatures_match_dispatch() {
assert!(NET_VTABLE.on_open.is_some());
assert!(NET_VTABLE.on_data.is_some());
assert!(NET_VTABLE.on_writable.is_some());
assert!(NET_VTABLE.on_close.is_some());
assert!(NET_VTABLE.on_end.is_some());
assert!(NET_VTABLE.on_fd.is_none());
}
#[test]
fn test_net_pending_write_large_data() {
let mut pw = NetPendingWrite::default();
let large: Vec<u8> = vec![0xAB; 1024 * 64]; pw.set_data(&large);
assert_eq!(pw.len, large.len());
assert!(!pw.is_empty());
pw.clear();
assert!(pw.is_empty());
}
#[test]
fn test_net_pending_write_reuse_buffer() {
let mut pw = NetPendingWrite::default();
pw.set_data(b"first_write");
assert_eq!(pw.len, 11);
pw.set_data(b"second");
assert_eq!(pw.len, 6);
pw.clear();
}
#[test]
fn test_net_cleanup_clears_all_thread_local_state() {
bao_uloop::force_link();
let loop_ = get_loop();
assert!(!loop_.is_null());
NET_SERVER_GROUPS.with(|g| {
let mut group = Box::new(SocketGroup::default());
group.init(loop_, Some(&NET_VTABLE), ptr::null_mut());
g.borrow_mut().insert(9999, group);
});
NET_LISTEN_SOCKETS.with(|l| l.borrow_mut().push(9999));
NET_SOCKETS.with(|s| s.borrow_mut().insert(9998, true));
NET_SERVER_GROUPS.with(|g| assert!(!g.borrow().is_empty()));
NET_SOCKETS.with(|s| assert!(!s.borrow().is_empty()));
let cleanup = NetCleanup;
drop(cleanup);
NET_SERVER_GROUPS.with(|g| assert!(g.borrow().is_empty()));
NET_LISTEN_SOCKETS.with(|l| assert!(l.borrow().is_empty()));
NET_SOCKETS.with(|s| assert!(s.borrow().is_empty()));
}
#[test]
fn test_connect_result_initial_state() {
CONNECT_RESULT.with(|r| assert!(r.get().is_none(), "initial CONNECT_RESULT is None"));
CONNECT_ERROR.with(|e| assert!(!e.get(), "initial CONNECT_ERROR is false"));
}
#[test]
fn test_connect_result_set_and_reset() {
CONNECT_RESULT.with(|r| r.set(Some(42)));
assert_eq!(CONNECT_RESULT.with(|r| r.get()), Some(42));
CONNECT_RESULT.with(|r| r.set(None));
assert!(CONNECT_RESULT.with(|r| r.get()).is_none());
}
#[test]
fn test_connect_error_set_and_reset() {
CONNECT_ERROR.with(|e| e.set(true));
assert!(CONNECT_ERROR.with(|e| e.get()));
CONNECT_ERROR.with(|e| e.set(false));
assert!(!CONNECT_ERROR.with(|e| e.get()));
}
#[test]
fn test_js_socket_methods_exist() {
assert!(NET_JS.contains("Socket.prototype.connect"));
assert!(NET_JS.contains("Socket.prototype.write"));
assert!(NET_JS.contains("Socket.prototype.end"));
assert!(NET_JS.contains("Socket.prototype.destroy"));
}
#[test]
fn test_js_server_methods_exist() {
assert!(NET_JS.contains("Server.prototype.listen"));
assert!(NET_JS.contains("Server.prototype.close"));
assert!(NET_JS.contains("Server.prototype.address"));
}
#[test]
fn test_js_net_native_functions() {
assert!(NET_JS.contains("__net_listen"));
assert!(NET_JS.contains("__net_connect"));
assert!(NET_JS.contains("__net_write"));
assert!(NET_JS.contains("__net_close"));
}
#[test]
fn test_js_isip_validation_logic() {
assert!(NET_JS.contains("split(\".\")"));
assert!(NET_JS.contains("parts.length === 4"));
assert!(NET_JS.contains("parseInt"));
assert!(NET_JS.contains("0 <= n && n <= 255") || NET_JS.contains("n < 0 || n > 255"));
}
#[test]
fn test_net_socket_ext_default_is_zero() {
let ext = NetSocketExt {
is_client: 0,
pending_write: NetPendingWrite::default(),
};
assert_eq!(ext.is_client, 0);
assert!(ext.pending_write.is_empty());
}
#[test]
fn test_net_socket_ext_client_flag() {
let ext = NetSocketExt {
is_client: 1,
pending_write: NetPendingWrite::default(),
};
assert_eq!(ext.is_client, 1);
}
#[test]
fn test_thread_local_hashmap_operations() {
NET_SOCKETS.with(|m| {
let mut map = m.borrow_mut();
map.insert(100, true);
map.insert(200, true);
assert_eq!(map.len(), 2);
assert!(map.contains_key(&100));
assert!(map.contains_key(&200));
assert!(!map.contains_key(&300));
map.remove(&100);
assert_eq!(map.len(), 1);
});
NET_SOCKETS.with(|m| m.borrow_mut().clear());
}
#[test]
fn test_thread_local_listen_socket_vec_operations() {
NET_LISTEN_SOCKETS.with(|l| {
let mut list = l.borrow_mut();
list.push(500);
list.push(600);
assert_eq!(list.len(), 2);
assert!(list.contains(&500));
assert!(list.contains(&600));
let pos = list.iter().position(|&k| k == 500).unwrap();
list.swap_remove(pos);
assert_eq!(list.len(), 1);
});
NET_LISTEN_SOCKETS.with(|l| l.borrow_mut().clear());
}
#[test]
fn test_net_pending_write_drop_does_not_double_free() {
let mut pw = NetPendingWrite::default();
pw.set_data(b"test_data");
pw.clear();
pw.set_data(b"more_data");
drop(pw);
}
#[test]
fn test_multiple_server_groups_in_thread_local() {
bao_uloop::force_link();
let loop_ = get_loop();
assert!(!loop_.is_null());
let g1 = ensure_server_group(loop_);
let g2 = ensure_server_group(loop_);
assert!(!g1.is_null());
assert!(!g2.is_null());
assert_ne!(g1, g2, "each server should get a unique group");
NET_SERVER_GROUPS.with(|g| {
let mut map = g.borrow_mut();
map.insert(g1 as usize, unsafe { Box::from_raw(g1) });
map.insert(g2 as usize, unsafe { Box::from_raw(g2) });
assert_eq!(map.len(), 2);
});
let cleanup = NetCleanup;
drop(cleanup);
NET_SERVER_GROUPS.with(|g| assert!(g.borrow().is_empty()));
}
}