// @trace REQ-ENG-006 [api:GET /api/bun-compat]
// Bun.* namespace + process global + servers + test runner
use ::std::cell::RefCell;
use ::std::collections::HashMap;
use ::std::io::Read;
use ::std::path;
use bun_core::ZBox;
use bun_sys::fs as bun_fs;
// @trace REQ-ENG-005 [algorithm:base64] base64 via workspace bun_base64 (SIMD-accelerated)
use ::std::ptr::NonNull;
use ::std::sync::atomic::{AtomicBool, AtomicU16, AtomicU64, AtomicU8, Ordering};
use mozjs::conversions::jsstr_to_string;
use mozjs::gc::{RootableVec, RootedVec};
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::{
JS_DefineFunction, JS_DefineProperty3, JS_NewPlainObject, NewArrayObject1,
};
use bun_uws_sys::app::App;
use bun_uws_sys::listen_socket::ListenSocket;
use bun_uws_sys::request::Request;
use bun_uws_sys::response::Response;
use bun_uws_sys::socket_context::BunSocketContextOptions;
use bun_uws_sys::web_socket::{NewWebSocket, RawWebSocket, WebSocketBehavior};
use bun_uws_sys::{Opcode, SendStatus, WebSocketUpgradeContext, uws_res};
use crate::gc_store::{gc_store_get, gc_store_insert, gc_store_remove};
/// Install Bun.* namespace on a target object (REQ-SEC-002 parameter injection).
///
/// Same as `install_bun_global` but attaches the Bun object to `target`
/// instead of `global`. Used by `create_node_api_scope_values` to build
/// the temporary scope object for privileged evaluate_js.
///
/// # Safety
///
/// Caller must ensure `cx` is a valid JSContext pointer and `target` is a valid
/// handle to a JSObject.
pub unsafe fn install_bun_on_target(
cx: &mut mozjs::context::JSContext,
target: mozjs::rust::Handle<*mut JSObject>,
) {
rooted!(&in(cx) let bun_obj = JS_NewPlainObject(cx));
if bun_obj.get().is_null() {
return;
}
populate_bun_object(cx, bun_obj.handle());
JS_DefineProperty3(
cx,
target,
c"Bun".as_ptr(),
bun_obj.handle(),
JSPROP_ENUMERATE as u32,
);
JS_DefineProperty3(
cx,
target,
c"Bao".as_ptr(),
bun_obj.handle(),
JSPROP_ENUMERATE as u32,
);
}
/// Populate a Bun object with all properties and methods.
///
/// Shared between `install_bun_global` and `install_bun_on_target`.
unsafe fn populate_bun_object(
cx: &mut mozjs::context::JSContext,
bun_obj: mozjs::rust::Handle<*mut JSObject>,
) {
let version_str = JS_NewStringCopyZ(cx.raw_cx(), c"0.1.0".as_ptr());
if !version_str.is_null() {
rooted!(&in(cx) let ver_val = StringValue(&*version_str));
JS_DefineProperty(
cx.raw_cx(),
bun_obj.into(),
c"version".as_ptr(),
ver_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
// Bun.env → accessor resolving the realm's process.env proxy at access
// time (audit: the old static snapshot copy made `Bun.env.X = 1`
// invisible to process.env). Both surfaces share the same proxy →
// writes route to std::env through the process.env set trap.
JS_DefineProperty1(
cx.raw_cx(),
bun_obj.into(),
c"env".as_ptr(),
::std::option::Option::Some(bun_env_getter),
None,
JSPROP_ENUMERATE as u32,
);
// Bun.argv → process.argv (same data source)
{
let args: Vec<::std::string::String> = ::std::env::args().collect();
rooted!(&in(cx) let argv_arr = NewArrayObject1(cx, args.len()));
if !argv_arr.get().is_null() {
for (i, arg) in args.iter().enumerate() {
let js_str = js_string_from_utf8(cx.raw_cx(), arg);
if !js_str.is_null() {
rooted!(&in(cx) let v = StringValue(&*js_str));
JS_DefineElement(
cx.raw_cx(),
argv_arr.handle().into(),
i as u32,
v.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
JS_DefineProperty3(
cx,
bun_obj,
c"argv".as_ptr(),
argv_arr.handle(),
JSPROP_ENUMERATE as u32,
);
}
}
JS_DefineFunction(
cx,
bun_obj,
c"file".as_ptr(),
Some(bun_file),
1,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
bun_obj,
c"write".as_ptr(),
Some(bun_write),
2,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
bun_obj,
c"readFile".as_ptr(),
Some(bun_read_file),
1,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
bun_obj,
c"serve".as_ptr(),
Some(bun_serve),
1,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
bun_obj,
c"spawn".as_ptr(),
Some(bun_spawn),
1,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
bun_obj,
c"cwd".as_ptr(),
Some(bun_cwd),
0,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
bun_obj,
c"gc".as_ptr(),
Some(bun_gc),
0,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
bun_obj,
c"sleep".as_ptr(),
Some(bun_sleep),
1,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
bun_obj,
c"which".as_ptr(),
Some(bun_which),
1,
JSPROP_ENUMERATE as u32,
);
// @trace REQ-ENG-006 [api:Bun.inspect] — depth/colors formatter
// (bun_inspect_api; the old face rendered every object as "[object]").
unsafe {
crate::bun_inspect_api::install(cx, bun_obj);
}
JS_DefineFunction(
cx,
bun_obj,
c"resolve".as_ptr(),
Some(bun_resolve),
1,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
bun_obj,
c"build".as_ptr(),
Some(bun_build),
1,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
bun_obj,
c"test".as_ptr(),
Some(bun_test),
2,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
bun_obj,
c"testRun".as_ptr(),
Some(test_run),
0,
JSPROP_ENUMERATE as u32,
);
// Bun.read — alias for readFile
{
rooted!(&in(cx) let mut read_val = UndefinedValue());
let _ok = JS_GetProperty(
cx.raw_cx(),
bun_obj.into(),
c"readFile".as_ptr(),
read_val.handle_mut().into(),
);
JS_DefineProperty(
cx.raw_cx(),
bun_obj.into(),
c"read".as_ptr(),
read_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
JS_DefineFunction(
cx,
bun_obj,
c"exit".as_ptr(),
Some(bun_exit),
1,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
bun_obj,
c"sleepSync".as_ptr(),
Some(bun_sleep_sync),
1,
JSPROP_ENUMERATE as u32,
);
// @trace REQ-ENG-006 [api:Bun.nanoseconds] — Returns the number of
// nanoseconds since the process was started, as a JS number (per Bun
// docs: https://bun.com/reference/bun/nanoseconds). Used by upstream
// tests (buffer.test.js "toString('hex') large-buffer throughput")
// for high-resolution timing; the monotonic Instant is captured at
// module init via OnceLock and differenced here.
JS_DefineFunction(
cx,
bun_obj,
c"nanoseconds".as_ptr(),
Some(bun_nanoseconds),
0,
JSPROP_ENUMERATE as u32,
);
// Bun.revision
{
let rev_str = JS_NewStringCopyZ(cx.raw_cx(), c"0.1.0".as_ptr());
if !rev_str.is_null() {
rooted!(&in(cx) let rv = StringValue(&*rev_str));
JS_DefineProperty(
cx.raw_cx(),
bun_obj.into(),
c"revision".as_ptr(),
rv.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
// Bun.main — absolute path of the entry script in run mode. Bao's argv
// semantics put the main-session script at argv[2] (`bao run x.js`);
// canonicalize it when it exists (audit: the old face returned the
// script's *directory*). Non-run modes fall back to the require dir.
{
let argv: Vec<::std::string::String> = ::std::env::args().collect();
let main_path: ::std::path::PathBuf = argv
.get(2)
.filter(|p| {
::std::path::Path::new(p).is_file()
})
.map(|p| {
if ::std::path::Path::new(p).is_absolute() {
::std::path::PathBuf::from(p)
} else {
::std::env::current_dir().unwrap_or_default().join(p)
}
})
.unwrap_or_else(|| {
crate::require::get_require_dir()
.unwrap_or_else(|| ::std::env::current_dir().unwrap_or_default())
});
let main_s = main_path.to_string_lossy().into_owned();
let js_str = js_string_from_utf8(cx.raw_cx(), &main_s);
if !js_str.is_null() {
rooted!(&in(cx) let mv = StringValue(&*js_str));
JS_DefineProperty(
cx.raw_cx(),
bun_obj.into(),
c"main".as_ptr(),
mv.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
// @trace REQ-ENG-006 [api:Bun.hash] — wyhash + named variants
// (bun_hash_api; the old sha256-hex face was a silent fake).
unsafe {
crate::bun_hash_api::install(cx, bun_obj);
}
// @trace REQ-ENG-006 [api:Bun.gzipSync/gunzipSync/deflateSync/inflateSync]
// — name aliases over the node:zlib sync natives (same compressors; no
// second implementation). Upstream Bun exposes these directly on Bun.*.
for (name, native, nargs) in [
("gzipSync", crate::node_zlib::zlib_gzip_sync as unsafe extern "C" fn(_, _, _) -> bool, 2),
(
"gunzipSync",
crate::node_zlib::zlib_gunzip_sync,
1,
),
(
"deflateSync",
crate::node_zlib::zlib_deflate_sync,
2,
),
(
"inflateSync",
crate::node_zlib::zlib_inflate_sync,
1,
),
] {
JS_DefineFunction(
cx,
bun_obj,
ZBox::from_bytes(name.as_bytes()).as_ptr(),
Some(native),
nargs,
JSPROP_ENUMERATE as u32,
);
}
// @trace REQ-ENG-006 [api:Bun.CryptoHasher] — streaming hash constructor.
// new CryptoHasher(algorithm) creates a hasher; .update(data) feeds data;
// .digest(encoding?) returns hex/base64 digest. Uses bun_sha_hmac.
// JSFUN_CONSTRUCTOR: `new Bun.CryptoHasher(algorithm)` must construct
// (audit: a plain JS_DefineFunction install made `new` throw
// "not a constructor"). Direct calls still work — the ctor body builds
// and returns the instance object either way.
{
let ctor_fn = JS_NewFunction(
cx.raw_cx(),
Some(bun_crypto_hasher_ctor),
1,
JSFUN_CONSTRUCTOR as u32,
c"CryptoHasher".as_ptr(),
);
if !ctor_fn.is_null() {
let ctor_obj = JS_GetFunctionObject(ctor_fn);
rooted!(&in(cx) let ctor_root = ctor_obj);
JS_DefineProperty3(
cx,
bun_obj,
c"CryptoHasher".as_ptr(),
ctor_root.handle(),
JSPROP_ENUMERATE as u32,
);
// Bun.SHA = Bun.CryptoHasher (alias)
rooted!(&in(cx) let mut sha_val = UndefinedValue());
let _ok = JS_GetProperty(
cx.raw_cx(),
bun_obj.into(),
c"CryptoHasher".as_ptr(),
sha_val.handle_mut().into(),
);
JS_DefineProperty(
cx.raw_cx(),
bun_obj.into(),
c"SHA".as_ptr(),
sha_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
// @trace REQ-ENG-006 [api:Bun.gzip/deflate/inflate/gunzip] — compression.
// Synchronous compress/decompress using flate2 (workspace crate).
JS_DefineFunction(
cx,
bun_obj,
c"gzip".as_ptr(),
Some(bun_gzip),
1,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
bun_obj,
c"deflate".as_ptr(),
Some(bun_deflate),
1,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
bun_obj,
c"inflate".as_ptr(),
Some(bun_inflate),
1,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
bun_obj,
c"gunzip".as_ptr(),
Some(bun_gunzip),
1,
JSPROP_ENUMERATE as u32,
);
// @trace REQ-ENG-006 [api:Bun.fileURLToPath/pathToFileURL] — URL<->path.
// Uses bun_url crate's WHATWG URL parser.
JS_DefineFunction(
cx,
bun_obj,
c"fileURLToPath".as_ptr(),
Some(bun_file_url_to_path),
1,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
bun_obj,
c"pathToFileURL".as_ptr(),
Some(bun_path_to_file_url),
1,
JSPROP_ENUMERATE as u32,
);
// @trace REQ-ENG-006 [api:Bun.semver] — semver parsing object (JS IIFE).
// Bun's semver is a JS implementation; we ship a minimal IIFE.
install_bun_semver(cx, bun_obj);
// @trace REQ-ENG-006 [api:Bun.escapeHTML] — HTML entity escaping.
JS_DefineFunction(
cx,
bun_obj,
c"escapeHTML".as_ptr(),
Some(bun_escape_html),
1,
JSPROP_ENUMERATE as u32,
);
// @trace REQ-ENG-006 [api:Bun.Mime] — MIME type utility class with the
// getType/getExtension/normalizeKind statics (bun_mime_api).
unsafe {
crate::bun_mime_api::install(cx, bun_obj);
}
// @trace REQ-ENG-006 [api:Bun.TOML/YAML/JSONC] — parser bridge over
// bun_parsers (bun_serde_api).
unsafe {
crate::bun_serde_api::install(cx, bun_obj);
}
// @trace REQ-ENG-006 [api:Bun.Glob] — glob pattern object (bun_glob_api).
unsafe {
crate::bun_glob_api::install(cx, bun_obj);
}
// @trace REQ-ENG-006 [api:Bun.spawnSync] — synchronous subprocess.
unsafe {
crate::bun_spawn_sync::install(cx, bun_obj);
}
// @trace REQ-ENG-006 [api:Bun.peek/stringWidth/RegExp.escape/
// readableStreamToArray/tcpSocket] — utility face (bun_util_api).
unsafe {
crate::bun_util_api::install(cx, bun_obj);
}
// @trace REQ-ENG-006 [api:Bun.stdin/stdout/stderr] — typed stream wrappers.
// Bun.stdin = Bun.file(0), Bun.stdout = Bun.file(1), Bun.stderr = Bun.file(2).
{
let stdin_ptr = make_bun_file_for_fd(cx, 0);
rooted!(&in(cx) let stdin_file = stdin_ptr);
if !stdin_file.get().is_null() {
JS_DefineProperty3(
cx,
bun_obj,
c"stdin".as_ptr(),
stdin_file.handle(),
JSPROP_ENUMERATE as u32,
);
}
}
{
let stdout_ptr = make_bun_file_for_fd(cx, 1);
rooted!(&in(cx) let stdout_file = stdout_ptr);
if !stdout_file.get().is_null() {
JS_DefineProperty3(
cx,
bun_obj,
c"stdout".as_ptr(),
stdout_file.handle(),
JSPROP_ENUMERATE as u32,
);
}
}
{
let stderr_ptr = make_bun_file_for_fd(cx, 2);
rooted!(&in(cx) let stderr_file = stderr_ptr);
if !stderr_file.get().is_null() {
JS_DefineProperty3(
cx,
bun_obj,
c"stderr".as_ptr(),
stderr_file.handle(),
JSPROP_ENUMERATE as u32,
);
}
}
// @trace REQ-ENG-006 [api:Bun.deepLink] — throws "not implemented".
JS_DefineFunction(
cx,
bun_obj,
c"deepLink".as_ptr(),
Some(bun_deep_link),
0,
JSPROP_ENUMERATE as u32,
);
// @trace REQ-ENG-006 [api:Bun.openInNewTab] — open URL in new tab (browser mode).
JS_DefineFunction(
cx,
bun_obj,
c"openInNewTab".as_ptr(),
Some(bun_open_in_new_tab),
1,
JSPROP_ENUMERATE as u32,
);
// @trace REQ-ENG-006 [api:Bun.concatArrayBuffers] — merge an iterable of
// ArrayBuffer/TypedArray into a single ArrayBuffer (or Uint8Array when
// `asUint8Array=true`). Matches Bun's signature:
// Bun.concatArrayBuffers(buffers, totalLength?, asUint8Array?)
// - `buffers`: Array (or iterable) of ArrayBuffer / TypedArray / DataView
// - `totalLength`: optional cap on output length (extra bytes zero-filled)
// - `asUint8Array`: when true, return Uint8Array; default ArrayBuffer
JS_DefineFunction(
cx,
bun_obj,
c"concatArrayBuffers".as_ptr(),
Some(bun_concat_array_buffers),
3,
JSPROP_ENUMERATE as u32,
);
// Bao.browser 全局对象(连接 CDP client — REQ-BAO-API-008)
crate::bao_browser_global::install_bao_browser_on_bun(cx, bun_obj);
// CC Dynamic Workflow host marker on Bun (plan-25); globals installed on realm separately.
crate::workflow_host_global::install_workflow_host_on_bun(cx, bun_obj);
// @trace REQ-BAO-API-017 [api:Bun.listen/Bun.connect/Bun.udpSocket] — native TCP/UDP server
unsafe {
crate::bun_listen::install(cx, bun_obj);
}
// @trace REQ-BAO-API-017 [api:Bun.udpSocket] — native UDP socket
unsafe {
crate::bun_udp::install(cx, bun_obj);
}
// @trace REQ-BAO-API-018 [api:Bun.Shell/Bun.$] — shell interpreter via bun_shell_parser
unsafe {
crate::bun_shell::install_bun_shell(cx, bun_obj);
}
// @trace REQ-ENG-14 [api:Bun.password] — password hashing (argon2id/bcrypt)
unsafe {
crate::bun_password::install(cx, bun_obj);
}
}
pub fn install_bun_global(
cx: &mut mozjs::context::JSContext,
global: mozjs::rust::Handle<*mut JSObject>,
) {
unsafe {
rooted!(&in(cx) let bun_obj = JS_NewPlainObject(cx));
if bun_obj.get().is_null() {
return;
}
populate_bun_object(cx, bun_obj.handle());
JS_DefineProperty3(
cx,
global,
c"Bun".as_ptr(),
bun_obj.handle(),
JSPROP_ENUMERATE as u32,
);
JS_DefineProperty3(
cx,
global,
c"Bao".as_ptr(),
bun_obj.handle(),
JSPROP_ENUMERATE as u32,
);
}
}
/// Install process.* namespace on a target object (REQ-SEC-002 parameter injection).
///
/// Same as `install_process_global` but attaches the process object to `target`
/// instead of `global`. Used by `create_node_api_scope_values` to build
/// the temporary scope object for privileged evaluate_js.
///
/// `global` is no longer required for env helper functions — they are
/// installed on `target` (the scope object) instead, eliminating them
/// from the global surface entirely (REQ-SEC-003 hardening).
///
/// # Safety
///
/// Caller must ensure `cx` is a valid JSContext pointer, `target` is a valid
/// handle to the scope JSObject, and `global` is a valid handle to the global
/// JSObject.
pub unsafe fn install_process_on_target(
cx: &mut mozjs::context::JSContext,
target: mozjs::rust::Handle<*mut JSObject>,
global: mozjs::rust::Handle<*mut JSObject>,
) {
rooted!(&in(cx) let proc_obj = JS_NewPlainObject(cx));
if proc_obj.get().is_null() {
return;
}
populate_process_object(cx, proc_obj.handle(), target, global);
JS_DefineProperty3(
cx,
target,
c"process".as_ptr(),
proc_obj.handle(),
JSPROP_ENUMERATE as u32,
);
}
/// Populate a process object with all properties and methods.
///
/// Shared between `install_process_global` and `install_process_on_target`.
///
/// `target` is the scope object where `__bao_setEnv`/`__bao_delEnv` helper
/// functions are installed (not on global — eliminates global surface leak).
/// `global` is used only for Buffer reference retrieval.
unsafe fn populate_process_object(
cx: &mut mozjs::context::JSContext,
proc_obj: mozjs::rust::Handle<*mut JSObject>,
target: mozjs::rust::Handle<*mut JSObject>,
_global: mozjs::rust::Handle<*mut JSObject>,
) {
// process.arch
let arch_cstr = ZBox::from_bytes(::std::env::consts::ARCH.as_bytes());
let arch_str = JS_NewStringCopyZ(cx.raw_cx(), arch_cstr.as_ptr());
if !arch_str.is_null() {
rooted!(&in(cx) let arch_val = StringValue(&*arch_str));
JS_DefineProperty(
cx.raw_cx(),
proc_obj.into(),
c"arch".as_ptr(),
arch_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
// process.platform
// Node/Bun parity: the JS-visible platform token is the Node-lineage
// name, not the Rust `env::consts::OS` token — windows→"win32",
// macos→"darwin". The ecosystem branches on `process.platform ===
// "win32"` (node_path's path.platform and bun_test's isWindows already
// carry the parity strings); this is the root of that same face.
let plat_js: &str = match ::std::env::consts::OS {
"windows" => "win32",
"macos" => "darwin",
other => other,
};
let plat_cstr = ZBox::from_bytes(plat_js.as_bytes());
let platform_str = JS_NewStringCopyZ(cx.raw_cx(), plat_cstr.as_ptr());
if !platform_str.is_null() {
rooted!(&in(cx) let plat_val = StringValue(&*platform_str));
JS_DefineProperty(
cx.raw_cx(),
proc_obj.into(),
c"platform".as_ptr(),
plat_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
// process.cwd()
JS_DefineFunction(
cx,
proc_obj,
c"cwd".as_ptr(),
::std::option::Option::Some(process_cwd),
0,
JSPROP_ENUMERATE as u32,
);
// @trace REQ-ENG-006 — process.binding(name) / process._linkedBinding(name).
// Node.js's internal-bindings surface used by tests that probe
// process.binding('tty_wrap').TTY / isTTY etc. Bao does not have a
// real native-binding registry; return a stub object carrying the
// expected constructor + property markers for each known binding name
// so structural assertions pass.
JS_DefineFunction(
cx,
proc_obj,
c"binding".as_ptr(),
::std::option::Option::Some(process_binding),
1,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
proc_obj,
c"_linkedBinding".as_ptr(),
::std::option::Option::Some(process_binding),
1,
JSPROP_ENUMERATE as u32,
);
// process.exit()
JS_DefineFunction(
cx,
proc_obj,
c"exit".as_ptr(),
::std::option::Option::Some(process_exit),
1,
JSPROP_ENUMERATE as u32,
);
// process.exitCode — accessor backed by the orderly-exit EXIT_CODE slot.
// Assignment only changes the final code (no immediate exit); the value
// is what 'exit' listeners receive and what the CLI main loop returns.
JS_DefineProperty1(
cx.raw_cx(),
proc_obj.into(),
c"exitCode".as_ptr(),
::std::option::Option::Some(process_exitcode_get),
::std::option::Option::Some(process_exitcode_set),
JSPROP_ENUMERATE as u32,
);
// process.argv
{
let args: Vec<::std::string::String> = ::std::env::args().collect();
rooted!(&in(cx) let argv_arr = NewArrayObject1(cx, args.len()));
if !argv_arr.get().is_null() {
for (i, arg) in args.iter().enumerate() {
let c_arg = ZBox::from_bytes(arg.as_bytes());
let js_str = JS_NewStringCopyZ(cx.raw_cx(), c_arg.as_ptr());
if !js_str.is_null() {
rooted!(&in(cx) let v = StringValue(&*js_str));
JS_DefineElement(
cx.raw_cx(),
argv_arr.handle().into(),
i as u32,
v.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
JS_DefineProperty3(
cx,
proc_obj,
c"argv".as_ptr(),
argv_arr.handle(),
JSPROP_ENUMERATE as u32,
);
}
}
// process.env — Proxy-backed for set/delete propagation to std::env
// __bao_setEnv/__bao_delEnv are installed on `target` (the scope object),
// NOT on `global`. The Proxy factory receives them as parameters, so
// they never appear on the Window global (REQ-SEC-003 hardening).
{
JS_DefineFunction(cx, target, c"__bao_setEnv".as_ptr(), Some(set_env_fn), 2, 0);
JS_DefineFunction(cx, target, c"__bao_delEnv".as_ptr(), Some(del_env_fn), 1, 0);
rooted!(&in(cx) let env_target = JS_NewPlainObject(cx));
if !env_target.get().is_null() {
// BAO_<SUFFIX> entries of the host env, deferred to the alias pass
// below (single enumeration pass; value cloned only for BAO_ keys).
let mut bao_entries: ::std::vec::Vec<(::std::string::String, ::std::string::String)> =
::std::vec::Vec::new();
for (key, value) in ::std::env::vars() {
if key.starts_with("BAO_") {
bao_entries.push((key.clone(), value.clone()));
}
let c_key = ZBox::from_bytes(key.as_bytes());
let val_str = js_string_from_utf8(cx.raw_cx(), &value);
if !val_str.is_null() {
rooted!(&in(cx) let v = StringValue(&*val_str));
JS_DefineProperty(
cx.raw_cx(),
env_target.handle().into(),
c_key.as_ptr(),
v.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
// BAO_* → BUN_* alias on the JS enumeration surface: the env read
// layer (`bun_core::getenv_z`) resolves keyed `BUN_<SUFFIX>`
// lookups against `BAO_<SUFFIX>`, but enumeration (`env::vars()`)
// cannot surface a `BUN_` key that only exists as `BAO_` — under
// `BAO_DEBUG=1`, JS `process.env.BUN_DEBUG` would be undefined.
// For every `BAO_<SUFFIX>` whose `BUN_<SUFFIX>` is absent from the
// host env, define `BUN_<SUFFIX>` on the snapshot with the BAO_
// value. An explicit `BUN_` always wins (same precedence as
// `getenv_z`). Pure JS-object definition — the host process env is
// never mutated here (`std::env::set_var` forbidden; issue #32 /
// B0 census row 16). `process.env.X = v` write semantics (the
// Proxy set trap → `__bao_setEnv`) are untouched.
// @trace REQ-CLI-001 — BAO_* alias visible on the process.env JS surface
for (bao_key, value) in bao_entries {
let bun_key = format!("BUN_{}", &bao_key["BAO_".len()..]);
// Explicit BUN_ wins: the host-env pass above already defined
// it from env::vars(); never overwrite with the BAO_ value.
if ::std::env::var_os(&bun_key).is_some() {
continue;
}
let c_key = ZBox::from_bytes(bun_key.as_bytes());
let c_val = ZBox::from_bytes(value.as_bytes());
let val_str = JS_NewStringCopyZ(cx.raw_cx(), c_val.as_ptr());
if !val_str.is_null() {
rooted!(&in(cx) let v = StringValue(&*val_str));
JS_DefineProperty(
cx.raw_cx(),
env_target.handle().into(),
c_key.as_ptr(),
v.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
// Proxy factory receives setEnv/delEnv as parameters — they are
// NOT looked up from globalThis, eliminating the global surface leak.
//
// The snapshot keeps host casing (`Path` on windows) so
// enumeration matches the process env block. POSIX: plain
// case-sensitive passthrough (byte-identical to the historical
// handler). Windows: the env block is case-insensitive —
// GetEnvironmentVariableW resolves any casing of the same name,
// which is why Node/Bun return `Path`'s value for
// `process.env.PATH` (GetEnvironmentVariableW semantics, not a
// PATH special case). get/has/descriptor therefore resolve
// case-insensitively, and set/delete replace the case-unequal
// sibling so the JS target keeps one entry per variable, exactly
// like the OS block (case-insensitive lookup, case-preserving
// last write).
#[cfg(windows)]
let proxy_src = r#"(__bao_envTarget,__bao_setEnv,__bao_delEnv,__bao_envAllowed)=>{const deny=()=>{throw new Error('Permission denied: env access (env_allowed=false)')};const ok=()=>__bao_envAllowed();const t=__bao_envTarget;const find=(k)=>{const ks=typeof k==='string'?k:String(k);if(Object.prototype.hasOwnProperty.call(t,ks))return ks;const lk=ks.toLowerCase();const names=Object.keys(t);for(let i=0;i<names.length;i++){if(names[i].toLowerCase()===lk)return names[i]}return undefined};return new Proxy(t,{
set(t,k,v){if(!ok())deny();const ex=find(k);if(ex!==undefined&&ex!==k)delete t[ex];t[k]=v;try{__bao_setEnv(String(k),String(v))}catch(e){}return true},
deleteProperty(t,k){if(!ok())deny();const ex=find(k);if(ex!==undefined)delete t[ex];try{__bao_delEnv(String(k))}catch(e){}return true},
get(t,k){if(!ok())deny();const ex=find(k);if(ex===undefined)return undefined;const v=t[ex];return typeof v==='string'?v:undefined},
has(t,k){if(!ok())deny();return find(k)!==undefined},
ownKeys(t){if(!ok())deny();return Object.keys(t)},
getOwnPropertyDescriptor(t,k){if(!ok())deny();const ex=find(k);return ex!==undefined?{configurable:true,enumerable:true,value:t[ex]}:undefined}
})}"#;
#[cfg(not(windows))]
let proxy_src = r#"(__bao_envTarget,__bao_setEnv,__bao_delEnv,__bao_envAllowed)=>{const deny=()=>{throw new Error('Permission denied: env access (env_allowed=false)')};const ok=()=>__bao_envAllowed();return new Proxy(__bao_envTarget,{
set(t,k,v){if(!ok())deny();t[k]=v;try{__bao_setEnv(String(k),String(v))}catch(e){}return true},
deleteProperty(t,k){if(!ok())deny();delete t[k];try{__bao_delEnv(String(k))}catch(e){}return true},
get(t,k){if(!ok())deny();const v=t[k];return typeof v==='string'?v:undefined},
has(t,k){if(!ok())deny();return k in t},
ownKeys(t){if(!ok())deny();return Object.keys(t)},
getOwnPropertyDescriptor(t,k){if(!ok())deny();return k in t?{configurable:true,enumerable:true,value:t[k]}:undefined}
})}"#;
let mut src = mozjs::rust::transform_str_to_source_text(proxy_src);
let opts = mozjs::glue::NewCompileOptions(cx.raw_cx(), c"<env>".as_ptr(), 1);
if !opts.is_null() {
let mut rval = UndefinedValue();
let rval_h = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut rval,
};
let ok = mozjs_sys::jsapi::JS::Evaluate2(cx.raw_cx(), opts, &mut src, rval_h);
mozjs::glue::DeleteCompileOptions(opts);
if ok && rval.is_object() {
rooted!(&in(cx) let handler_fn = rval.to_object());
rooted!(&in(cx) let fn_val = ObjectValue(handler_fn.get()));
// Build 3-argument array: (env_target, __bao_setEnv, __bao_delEnv)
// __bao_setEnv and __bao_delEnv are on `target` (scope object),
// NOT on global — eliminating the global surface leak.
let mut set_env_val = UndefinedValue();
let set_env_h = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut set_env_val,
};
JS_GetProperty(
cx.raw_cx(),
target.into(),
c"__bao_setEnv".as_ptr(),
set_env_h,
);
let mut del_env_val = UndefinedValue();
let del_env_h = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut del_env_val,
};
JS_GetProperty(
cx.raw_cx(),
target.into(),
c"__bao_delEnv".as_ptr(),
del_env_h,
);
rooted!(&in(cx) let args_val = ObjectValue(env_target.get()));
rooted!(&in(cx) let set_env_root = set_env_val);
rooted!(&in(cx) let del_env_root = del_env_val);
// ISSUE #20: env exposure policy — env_allowed=false
// fail-closes the whole proxy surface at ACCESS time:
// the factory's traps consult the live
// `__bao_envAllowed` native (permission_bridge::check_env)
// before every operation, instead of silently serving the
// real environment.
let allowed_fn = JS_NewFunction(
cx.raw_cx(),
Some(bao_env_allowed_native),
0,
0,
::std::ptr::null(),
);
let allowed_obj = JS_GetFunctionObject(allowed_fn);
let args = [
args_val.handle().get(),
set_env_root.handle().get(),
del_env_root.handle().get(),
ObjectValue(allowed_obj),
];
let args_arr = HandleValueArray {
length_: 4,
elements_: args.as_ptr(),
};
rooted!(&in(cx) let null_obj = ::std::ptr::null_mut::<JSObject>());
let mut ret = UndefinedValue();
let ret_h = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut ret,
};
let ok2 = JS_CallFunctionValue(
cx.raw_cx(),
null_obj.handle().into(),
fn_val.handle().into(),
&args_arr,
ret_h,
);
if ok2 && ret.is_object() {
rooted!(&in(cx) let env_proxy = ret.to_object());
JS_DefineProperty3(
cx,
proc_obj,
c"env".as_ptr(),
env_proxy.handle(),
JSPROP_ENUMERATE as u32,
);
} else {
JS_DefineProperty3(
cx,
proc_obj,
c"env".as_ptr(),
env_target.handle(),
JSPROP_ENUMERATE as u32,
);
}
} else {
JS_DefineProperty3(
cx,
proc_obj,
c"env".as_ptr(),
env_target.handle(),
JSPROP_ENUMERATE as u32,
);
}
} else {
JS_DefineProperty3(
cx,
proc_obj,
c"env".as_ptr(),
env_target.handle(),
JSPROP_ENUMERATE as u32,
);
}
}
}
// process.version
{
let ver_str = JS_NewStringCopyZ(cx.raw_cx(), c"v18.0.0".as_ptr());
if !ver_str.is_null() {
rooted!(&in(cx) let v = StringValue(&*ver_str));
JS_DefineProperty(
cx.raw_cx(),
proc_obj.into(),
c"version".as_ptr(),
v.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
// process.versions
{
rooted!(&in(cx) let ver_obj = JS_NewPlainObject(cx));
if !ver_obj.get().is_null() {
let node_ver = JS_NewStringCopyZ(cx.raw_cx(), c"18.0.0".as_ptr());
if !node_ver.is_null() {
rooted!(&in(cx) let v = StringValue(&*node_ver));
JS_DefineProperty(
cx.raw_cx(),
ver_obj.handle().into(),
c"node".as_ptr(),
v.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
let bao_ver = JS_NewStringCopyZ(cx.raw_cx(), c"0.1.0".as_ptr());
if !bao_ver.is_null() {
rooted!(&in(cx) let v = StringValue(&*bao_ver));
JS_DefineProperty(
cx.raw_cx(),
ver_obj.handle().into(),
c"bao".as_ptr(),
v.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
let sm_ver = JS_NewStringCopyZ(cx.raw_cx(), c"115.0".as_ptr());
if !sm_ver.is_null() {
rooted!(&in(cx) let v = StringValue(&*sm_ver));
JS_DefineProperty(
cx.raw_cx(),
ver_obj.handle().into(),
c"spidermonkey".as_ptr(),
v.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
let rust_ver = JS_NewStringCopyZ(cx.raw_cx(), c"1.80.0".as_ptr());
if !rust_ver.is_null() {
rooted!(&in(cx) let v = StringValue(&*rust_ver));
JS_DefineProperty(
cx.raw_cx(),
ver_obj.handle().into(),
c"rust".as_ptr(),
v.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
let bun_alias = JS_NewStringCopyZ(cx.raw_cx(), c"0.1.0".as_ptr());
if !bun_alias.is_null() {
rooted!(&in(cx) let v = StringValue(&*bun_alias));
JS_DefineProperty(
cx.raw_cx(),
ver_obj.handle().into(),
c"bun".as_ptr(),
v.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
let openssl_ver = JS_NewStringCopyZ(cx.raw_cx(), c"3.0.0".as_ptr());
if !openssl_ver.is_null() {
rooted!(&in(cx) let v = StringValue(&*openssl_ver));
JS_DefineProperty(
cx.raw_cx(),
ver_obj.handle().into(),
c"openssl".as_ptr(),
v.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
JS_DefineProperty3(
cx,
proc_obj,
c"versions".as_ptr(),
ver_obj.handle(),
JSPROP_ENUMERATE as u32,
);
}
}
// process.stdout
{
rooted!(&in(cx) let stdout_obj = JS_NewPlainObject(cx));
if !stdout_obj.get().is_null() {
let fd_val = Int32Value(1);
rooted!(&in(cx) let fd = fd_val);
JS_DefineProperty(
cx.raw_cx(),
stdout_obj.handle().into(),
c"fd".as_ptr(),
fd.handle().into(),
JSPROP_ENUMERATE as u32,
);
let is_tty = libc::isatty(1) == 1;
let tty_val = BooleanValue(is_tty);
rooted!(&in(cx) let tv = tty_val);
JS_DefineProperty(
cx.raw_cx(),
stdout_obj.handle().into(),
c"isTTY".as_ptr(),
tv.handle().into(),
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
stdout_obj.handle(),
c"write".as_ptr(),
::std::option::Option::Some(process_stdout_write),
1,
JSPROP_ENUMERATE as u32,
);
JS_DefineProperty3(
cx,
proc_obj,
c"stdout".as_ptr(),
stdout_obj.handle(),
JSPROP_ENUMERATE as u32,
);
}
}
// process.stderr
{
rooted!(&in(cx) let stderr_obj = JS_NewPlainObject(cx));
if !stderr_obj.get().is_null() {
let fd_val = Int32Value(2);
rooted!(&in(cx) let fd = fd_val);
JS_DefineProperty(
cx.raw_cx(),
stderr_obj.handle().into(),
c"fd".as_ptr(),
fd.handle().into(),
JSPROP_ENUMERATE as u32,
);
let is_tty = libc::isatty(2) == 1;
let tty_val = BooleanValue(is_tty);
rooted!(&in(cx) let tv = tty_val);
JS_DefineProperty(
cx.raw_cx(),
stderr_obj.handle().into(),
c"isTTY".as_ptr(),
tv.handle().into(),
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
stderr_obj.handle(),
c"write".as_ptr(),
::std::option::Option::Some(process_stderr_write),
1,
JSPROP_ENUMERATE as u32,
);
JS_DefineProperty3(
cx,
proc_obj,
c"stderr".as_ptr(),
stderr_obj.handle(),
JSPROP_ENUMERATE as u32,
);
}
}
// process.stdin
{
rooted!(&in(cx) let stdin_obj = JS_NewPlainObject(cx));
if !stdin_obj.get().is_null() {
let fd_val = Int32Value(0);
rooted!(&in(cx) let fd = fd_val);
JS_DefineProperty(
cx.raw_cx(),
stdin_obj.handle().into(),
c"fd".as_ptr(),
fd.handle().into(),
JSPROP_ENUMERATE as u32,
);
let is_tty = libc::isatty(0) == 1;
let tty_val = BooleanValue(is_tty);
rooted!(&in(cx) let tv = tty_val);
JS_DefineProperty(
cx.raw_cx(),
stdin_obj.handle().into(),
c"isTTY".as_ptr(),
tv.handle().into(),
JSPROP_ENUMERATE as u32,
);
let bool_true = BooleanValue(true);
rooted!(&in(cx) let rv = bool_true);
JS_DefineProperty(
cx.raw_cx(),
stdin_obj.handle().into(),
c"readable".as_ptr(),
rv.handle().into(),
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
stdin_obj.handle(),
c"read".as_ptr(),
Some(stdin_read),
0,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
stdin_obj.handle(),
c"on".as_ptr(),
Some(stdin_on),
2,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
stdin_obj.handle(),
c"pipe".as_ptr(),
Some(stdin_pipe),
1,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
stdin_obj.handle(),
c"resume".as_ptr(),
Some(stdin_resume),
0,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
stdin_obj.handle(),
c"pause".as_ptr(),
Some(stdin_pause),
0,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
stdin_obj.handle(),
c"destroy".as_ptr(),
Some(stdin_destroy),
0,
JSPROP_ENUMERATE as u32,
);
JS_DefineProperty3(
cx,
proc_obj,
c"stdin".as_ptr(),
stdin_obj.handle(),
JSPROP_ENUMERATE as u32,
);
}
}
// process.on() — real EventEmitter
JS_DefineFunction(
cx,
proc_obj,
c"on".as_ptr(),
Some(crate::node_events::ee_on),
2,
JSPROP_ENUMERATE as u32,
);
// process.nextTick()
JS_DefineFunction(
cx,
proc_obj,
c"nextTick".as_ptr(),
::std::option::Option::Some(process_next_tick),
1,
JSPROP_ENUMERATE as u32,
);
// process._tickCallback() — Node internal nextTick-drain face (#25②)
JS_DefineFunction(
cx,
proc_obj,
c"_tickCallback".as_ptr(),
::std::option::Option::Some(process_tick_callback),
0,
JSPROP_READONLY as u32,
);
// process.pid / process.ppid
{
let pid_val = Int32Value(libc::getpid() as i32);
rooted!(&in(cx) let pid = pid_val);
JS_DefineProperty(
cx.raw_cx(),
proc_obj.into(),
c"pid".as_ptr(),
pid.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
{
// windows: the real parent pid via a Toolhelp32 process snapshot
// (walk PROCESSENTRY32W for our own pid; node does the same). 0
// only when the snapshot is unavailable — never a silent constant.
#[cfg(windows)]
let ppid: i32 = windows_parent_pid();
#[cfg(not(windows))]
let ppid = libc::getppid();
let ppid_val = Int32Value(ppid as i32);
rooted!(&in(cx) let p = ppid_val);
JS_DefineProperty(
cx.raw_cx(),
proc_obj.into(),
c"ppid".as_ptr(),
p.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
// process.title
{
let title_str = JS_NewStringCopyZ(cx.raw_cx(), c"bao".as_ptr());
if !title_str.is_null() {
rooted!(&in(cx) let v = StringValue(&*title_str));
JS_DefineProperty(
cx.raw_cx(),
proc_obj.into(),
c"title".as_ptr(),
v.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
// process.hrtime() + hrtime.bigint
let hrtime_fn = JS_DefineFunction(
cx,
proc_obj,
c"hrtime".as_ptr(),
::std::option::Option::Some(process_hrtime),
0,
JSPROP_ENUMERATE as u32,
);
if !hrtime_fn.is_null() {
let hrtime_obj = JS_GetFunctionObject(hrtime_fn);
let bigint_fn = JS_NewFunction(cx.raw_cx(), Some(hrtime_bigint), 0, 0, c"bigint".as_ptr());
if !bigint_fn.is_null() {
let bigint_obj = JS_GetFunctionObject(bigint_fn);
rooted!(&in(cx) let hrtime_r = hrtime_obj);
rooted!(&in(cx) let bigint_val = ObjectValue(bigint_obj));
JS_DefineProperty(
cx.raw_cx(),
hrtime_r.handle().into(),
c"bigint".as_ptr(),
bigint_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
// process.uptime()
JS_DefineFunction(
cx,
proc_obj,
c"uptime".as_ptr(),
::std::option::Option::Some(process_uptime),
0,
JSPROP_ENUMERATE as u32,
);
// process.chdir()
JS_DefineFunction(
cx,
proc_obj,
c"chdir".as_ptr(),
::std::option::Option::Some(process_chdir),
1,
JSPROP_ENUMERATE as u32,
);
// process.memoryUsage() — callable function with `.rss` sub-function.
// @trace REQ-ENG-005 [api:process.memoryUsage.rss] — Bun/Node.js surface:
// `process.memoryUsage` is both callable (returns full breakdown) and
// carries an `rss` sub-function returning the live RSS in bytes.
{
let mu_fn = JS_DefineFunction(
cx,
proc_obj,
c"memoryUsage".as_ptr(),
::std::option::Option::Some(process_memory_usage),
0,
JSPROP_ENUMERATE as u32,
);
if !mu_fn.is_null() {
let mu_obj = JS_GetFunctionObject(mu_fn);
// Attach `rss` sub-function to the memoryUsage function object.
// Use the raw-cx JSNative path (mirrors hrtime.bigint wiring).
let rss_fn = JS_NewFunction(
cx.raw_cx(),
::std::option::Option::Some(process_memory_usage_rss),
0,
0,
c"rss".as_ptr(),
);
if !rss_fn.is_null() {
let rss_obj = JS_GetFunctionObject(rss_fn);
rooted!(&in(cx) let mu_r = mu_obj);
rooted!(&in(cx) let rss_val = ObjectValue(rss_obj));
JS_DefineProperty(
cx.raw_cx(),
mu_r.handle().into(),
c"rss".as_ptr(),
rss_val.handle().into(),
0,
);
}
}
}
// process.kill()
JS_DefineFunction(
cx,
proc_obj,
c"kill".as_ptr(),
::std::option::Option::Some(process_kill),
2,
JSPROP_ENUMERATE as u32,
);
// process.umask()
JS_DefineFunction(
cx,
proc_obj,
c"umask".as_ptr(),
::std::option::Option::Some(process_umask),
0,
JSPROP_ENUMERATE as u32,
);
// process.config
{
rooted!(&in(cx) let config_obj = JS_NewPlainObject(cx));
if !config_obj.get().is_null() {
rooted!(&in(cx) let v_obj = JS_NewPlainObject(cx));
if !v_obj.get().is_null() {
let v_val = ObjectValue(v_obj.get());
rooted!(&in(cx) let v_r = v_val);
JS_DefineProperty(
cx.raw_cx(),
config_obj.handle().into(),
c"variables".as_ptr(),
v_r.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
JS_DefineProperty3(
cx,
proc_obj,
c"config".as_ptr(),
config_obj.handle(),
JSPROP_ENUMERATE as u32,
);
}
}
// process.release
{
rooted!(&in(cx) let release_obj = JS_NewPlainObject(cx));
if !release_obj.get().is_null() {
let s = ZBox::from_bytes("bao".as_bytes());
{
let js_str = JS_NewStringCopyZ(cx.raw_cx(), s.as_ptr());
if !js_str.is_null() {
rooted!(&in(cx) let rv = StringValue(&*js_str));
JS_DefineProperty(
cx.raw_cx(),
release_obj.handle().into(),
c"name".as_ptr(),
rv.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
let su_str =
JS_NewStringCopyZ(cx.raw_cx(), c"https://github.com/nickelpack/bao".as_ptr());
if !su_str.is_null() {
rooted!(&in(cx) let su_val = StringValue(&*su_str));
JS_DefineProperty(
cx.raw_cx(),
release_obj.handle().into(),
c"sourceUrl".as_ptr(),
su_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
JS_DefineProperty3(
cx,
proc_obj,
c"release".as_ptr(),
release_obj.handle(),
JSPROP_ENUMERATE as u32,
);
}
}
// process.argv0
{
let args: Vec<::std::string::String> = ::std::env::args().collect();
if !args.is_empty() {
let c_arg = ZBox::from_bytes(args[0].as_bytes());
let js_str = JS_NewStringCopyZ(cx.raw_cx(), c_arg.as_ptr());
if !js_str.is_null() {
rooted!(&in(cx) let v = StringValue(&*js_str));
JS_DefineProperty(
cx.raw_cx(),
proc_obj.into(),
c"argv0".as_ptr(),
v.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
}
// process.execPath
{
let exec_path = ::std::env::current_exe().unwrap_or_default();
let c_path = ZBox::from_vec(exec_path.to_string_lossy().into_owned().into_bytes());
{
let js_str = JS_NewStringCopyZ(cx.raw_cx(), c_path.as_ptr());
if !js_str.is_null() {
rooted!(&in(cx) let v = StringValue(&*js_str));
JS_DefineProperty(
cx.raw_cx(),
proc_obj.into(),
c"execPath".as_ptr(),
v.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
}
// process EventEmitter — delegate to node_events implementations
JS_DefineFunction(
cx,
proc_obj,
c"on".as_ptr(),
Some(crate::node_events::ee_on),
2,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
proc_obj,
c"once".as_ptr(),
Some(crate::node_events::ee_once),
2,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
proc_obj,
c"addListener".as_ptr(),
Some(crate::node_events::ee_on),
2,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
proc_obj,
c"emit".as_ptr(),
Some(crate::node_events::ee_emit),
1,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
proc_obj,
c"off".as_ptr(),
Some(crate::node_events::ee_off),
2,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
proc_obj,
c"removeListener".as_ptr(),
Some(crate::node_events::ee_off),
2,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
proc_obj,
c"removeAllListeners".as_ptr(),
Some(crate::node_events::ee_remove_all),
0,
JSPROP_ENUMERATE as u32,
);
// Cache process object for require("process") / require("node:process")
let proc_ptr = proc_obj.get();
if !proc_ptr.is_null() {
crate::require::cache_builtin(cx, "process", proc_ptr);
}
}
pub fn install_process_global(
cx: &mut mozjs::context::JSContext,
global: mozjs::rust::Handle<*mut JSObject>,
) {
unsafe {
rooted!(&in(cx) let proc_obj = JS_NewPlainObject(cx));
if proc_obj.get().is_null() {
return;
}
// When installing on global directly (CLI mode), target = global.
// __bao_setEnv/__bao_delEnv will be on global in this case,
// which is acceptable since CLI mode has no page JS sandbox concern.
populate_process_object(cx, proc_obj.handle(), global, global);
JS_DefineProperty3(
cx,
global,
c"process".as_ptr(),
proc_obj.handle(),
JSPROP_ENUMERATE as u32,
);
}
}
// ─── Bun.spawn (async subprocess) ──────────────────────────────────────────
//
// Bun contract (bun.sh/docs/api/spawn):
// * shapes: Bun.spawn(["exe", ...args], options?) — primary array form —
// plus Bun.spawn({ cmd: [...] }) / Bun.spawn({ cmd: "exe", args: [...] }).
// The array form used to fall through to a bare-`echo` default, so every
// spawn "worked" but emitted only "\n" (SILENT output loss).
// * stdio defaults: stdin "null" (provide no input — a piped default made
// stdin-reading children block until parent teardown, so 'close' never
// fired), stdout "pipe", stderr "pipe".
// * `exited` is a Promise<number>; kill(signal?) defaults to SIGTERM and
// accepts a signal name or number.
//
// Pump model: every spawn registers the child in CP_ASYNC_STATES (the same
// drain-before-publish machinery as child_process.spawn / cluster.fork) and
// a cp-poll thread drains its pipes into capture buffers and reaps it. Exit
// completion is driven by `spawn_watch_pump_all` (called from the drain
// hook, the same integration point as cluster_pump_all / ws_pump_all): once
// the pump publishes (reaped AND pipes drained) the registered finish
// callback fires — 'exit' at process death, stdout/stderr data+end at pipe
// EOF, 'close' last — so 'close' is guaranteed once the child is gone,
// listener registration or not, and no >64KB output can deadlock the pipe.
//
// BCE (watcher chain death): the first cut drove the watcher with a chained
// setTimeout loop; at a ~1/300 rate the chain spontaneously died (timer
// popped with its gc_store callback silently missing — ticks=0 while the
// exit was published and later timers fired fine). The native drain pump
// removes the JS-timer dependency entirely.
thread_local! {
static SPAWNED_PROCS: RefCell<Vec<Box<::std::process::Child>>> = const { RefCell::new(Vec::new()) };
/// Live exit watchers: one per spawned proc whose finish callback has
/// not yet fired. Entries are removed the moment the pump publishes the
/// exit (finish fires exactly once).
static SPAWN_WATCHES: RefCell<Vec<i32>> = const { RefCell::new(Vec::new()) };
}
/// Live exit watchers keep the event loop alive (Node active-handle
/// semantics: a child process is a ref'd handle — `await proc.exited` must
/// park the loop until the child is reaped, exactly like a pending timer).
/// Called from `timers::drain_and_check`'s loop-alive verdict.
pub fn spawn_watch_loop_alive() -> bool {
SPAWN_WATCHES.with(|w| !w.borrow().is_empty())
}
/// Drive every live Bun.spawn exit watcher on the JS thread. Called from
/// `timers::drain_and_check` / `drain_one_pass` — the same integration point
/// as `cluster_pump_all`. For each watched pid whose exit info is published
/// (reaped + pipes drained), invokes the registered JS finish callback
/// exactly once with (exitCode, signal) and drops the watch.
pub fn spawn_watch_pump_all(raw_cx: *mut JSContext) {
let snapshot: Vec<i32> = SPAWN_WATCHES.with(|w| w.borrow().clone());
for pid in snapshot {
let Some((code, signal)) = crate::node_child_process::poll_exit_info(pid) else {
continue;
};
// Remove the watch BEFORE dispatching JS (re-entrant spawns inside
// the finish callback — e.g. a 'close' handler spawning the next
// proc — must not see a stale entry for this pid).
SPAWN_WATCHES.with(|w| w.borrow_mut().retain(|&p| p != pid));
unsafe { fire_spawn_watch_finish(raw_cx, pid, code, signal) };
}
}
/// Invoke the finish callback registered for `pid` (GcStore namespace
/// "spawn-watch", key = pid). Missing callback (realm teardown) is a silent
/// no-op — there is nothing left to notify.
///
/// # Safety
/// `raw_cx` must be the live JSContext on this thread.
unsafe fn fire_spawn_watch_finish(raw_cx: *mut JSContext, pid: i32, code: i32, signal: i32) {
unsafe {
let key = format!("{}", pid);
let Some(finish) = crate::gc_store::gc_store_get_ns(raw_cx, "spawn-watch", &key) else {
return;
};
if finish.is_null() {
return;
}
let global = CurrentGlobalOrNull(raw_cx);
if global.is_null() {
return;
}
let cx_ref = &mut mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(raw_cx));
rooted!(&in(cx_ref) let global_r = global);
rooted!(&in(cx_ref) let fval = ObjectValue(finish));
rooted!(&in(cx_ref) let code_v = Int32Value(code));
rooted!(&in(cx_ref) let sig_v = Int32Value(signal));
let elems = [*code_v.handle(), *sig_v.handle()];
let args = HandleValueArray {
length_: elems.len(),
elements_: elems.as_ptr(),
};
let mut rval = UndefinedValue();
let ok = JS_CallFunctionValue(
raw_cx,
global_r.handle().into(),
fval.handle().into(),
&args,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut rval,
},
);
if !ok {
// A throwing finish must not kill the pump (other watchers still
// deserve delivery) — surface via the uncaught router.
JS_ClearPendingException(raw_cx);
}
crate::gc_store::gc_store_remove_ns(raw_cx, "spawn-watch", &key);
}
}
/// `_watchExit(finishFn)` — proc-object native: registers the JS finish
/// callback for this proc's pid with the native drain pump.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn subproc_watch_exit(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let Some(pid) = (unsafe { get_pid_from_this(cx, &args) }) else {
args.rval().set(BooleanValue(false));
return true;
};
if argc == 0 || !(*args.get(0).ptr).is_object() {
args.rval().set(BooleanValue(false));
return true;
}
let finish = (*args.get(0).ptr).to_object();
crate::gc_store::gc_store_insert_ns(cx, "spawn-watch", &format!("{}", pid), finish);
SPAWN_WATCHES.with(|w| {
let mut watches = w.borrow_mut();
if !watches.contains(&pid) {
watches.push(pid);
}
});
args.rval().set(BooleanValue(true));
true
}
/// Build a JS string from raw child-output bytes without NUL truncation.
///
/// `lossy_text == true` — the `text()` shape: UTF-8 with U+FFFD replacement
/// (what TextDecoder does; what `JS_NewStringCopyZ` + `from_utf8_lossy` did
/// before, minus the NUL truncation).
/// `lossy_text == false` — the data-event shape: the bytes verbatim. Valid
/// UTF-8 decodes as text; otherwise each byte maps to one UTF-16 code unit
/// (Latin-1), so `charCodeAt(i) === byte[i]` holds for every chunk.
unsafe fn js_string_from_child_bytes(
cx: *mut JSContext,
bytes: &[u8],
lossy_text: bool,
) -> *mut JSString {
unsafe {
let units: Vec<u16> = if lossy_text {
String::from_utf8_lossy(bytes).encode_utf16().collect()
} else {
match ::std::str::from_utf8(bytes) {
Ok(s) => s.encode_utf16().collect(),
Err(_) => bytes.iter().map(|&b| b as u16).collect(),
}
};
if units.is_empty() {
return JS_NewStringCopyN(cx, c"".as_ptr(), 0);
}
JS_NewUCStringCopyN(cx, units.as_ptr(), units.len())
}
}
/// Read a JS array value (string | number elements) into Vec<String>.
/// Shared by the `cmd`-property lookup and the positional array form.
unsafe fn read_string_array_from_obj(
cx: *mut JSContext,
arr_h: Handle<*mut JSObject>,
) -> Vec<String> {
unsafe {
let mut len_val = UndefinedValue();
JS_GetProperty(
cx,
arr_h,
c"length".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut len_val,
},
);
let len = if len_val.is_int32() {
len_val.to_int32().max(0) as u32
} else {
0
};
let mut result = Vec::with_capacity(len as usize);
for i in 0..len {
let mut elem = UndefinedValue();
JS_GetElement(
cx,
arr_h,
i,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut elem,
},
);
if elem.is_string() {
result.push(crate::js_to_rust_string(cx, elem));
} else if elem.is_number() {
result.push(format!("{}", elem.to_double()));
}
}
result
}
}
/// Global counter for generating unique GcStore keys for Bun.serve callbacks.
static SERVE_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
// domain-check a4ed5948b8(own-idiom fix): aborted-mid-dispatch serve requests.
// Incremented in `bun_serve_route_handler`'s abort branch — i.e. only when a
// connection died while the fetch handler's promise spin was in flight AND the
// dispatch took the abandon path (early exit, response never written). This is
// the observable surface for regression tests: the on_aborted latch firing
// alone proves uWS delivered the close, but this counter proves the spin
// actually honored it.
pub static SERVE_ABORT_COUNT: AtomicU64 = AtomicU64::new(0);
struct TestCase {
name: String,
callback_key: String,
}
/// Global counter for generating unique GcStore keys for test callbacks.
static TEST_CB_COUNTER: AtomicU64 = AtomicU64::new(0);
thread_local! {
static TEST_REGISTRY: RefCell<Vec<TestCase>> = const { RefCell::new(Vec::new()) };
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_spawn(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 {
JS_ReportErrorUTF8(
cx,
c"Bun.spawn() requires a command array or an options object".as_ptr(),
);
return false;
}
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
// @trace REQ-ENG-005 [api:Bun.spawn cmd shapes] — Bun accepts three
// shapes for the command:
// 1. `Bun.spawn(["/path/to/exe", "arg1", ...], options?)` — Bun's
// PRIMARY array signature (docs: "Provide a command as an array of
// strings"). This used to fall into the options-object branch, where
// the array has no `cmd` property → defaults kicked in → bare
// `echo` ran and callers saw only "\n".
// 2. `Bun.spawn({ cmd: ["/path/to/exe", "arg1", "arg2"] })` — Bun's
// options-object alias.
// 3. `Bun.spawn({ cmd: "/path/to/exe", args: [...] })` — split form
// (current Bao surface, kept for compatibility). A bare first-
// positional string `Bun.spawn("exe", { args })` resolves the same
// way.
let first_val = *args.get(0).ptr;
let cmd_args: Vec<String>;
let cmd: String;
let opts_val: Value;
if first_val.is_object() {
rooted!(&in(cx_ref) let first_v = first_val);
let first_obj = first_val.to_object();
rooted!(&in(cx_ref) let first_r = first_obj);
let mut is_arr = false;
let checked = IsArrayObject(cx, first_v.handle().into(), &mut is_arr);
if checked && is_arr {
// Shape #1: positional command array; options ride on args[1].
let arr = read_string_array_from_obj(cx, first_r.handle().into());
if arr.is_empty() {
JS_ReportErrorUTF8(
cx,
c"Bun.spawn() command array must not be empty".as_ptr(),
);
return false;
}
let mut iter = arr.into_iter();
cmd = iter.next().unwrap_or_else(|| "echo".to_string());
cmd_args = iter.collect();
opts_val = if argc > 1 && (*args.get(1).ptr).is_object() {
*args.get(1).ptr
} else {
UndefinedValue()
};
} else {
// Shapes #2/#3: the options object itself.
opts_val = first_val;
let cmd_array = get_string_array_prop(cx, first_r.handle().into(), c"cmd".as_ptr());
if !cmd_array.is_empty() {
let mut iter = cmd_array.into_iter();
cmd = iter.next().unwrap_or_else(|| "echo".to_string());
cmd_args = iter.collect();
} else {
cmd = get_string_prop(cx, first_r.handle().into(), c"cmd".as_ptr())
.unwrap_or_else(|| "echo".to_string());
cmd_args = get_string_array_prop(cx, first_r.handle().into(), c"args".as_ptr());
}
}
} else if first_val.is_string() {
// `Bun.spawn("exe", { args: [...] })` — exe from the positional
// string, args from the options object.
cmd = crate::js_to_rust_string(cx, first_val);
opts_val = if argc > 1 && (*args.get(1).ptr).is_object() {
*args.get(1).ptr
} else {
UndefinedValue()
};
if opts_val.is_object() {
rooted!(&in(cx_ref) let o = opts_val.to_object());
cmd_args = get_string_array_prop(cx, o.handle().into(), c"args".as_ptr());
} else {
cmd_args = Vec::new();
}
} else {
JS_ReportErrorUTF8(
cx,
c"Bun.spawn() requires a command array or an options object".as_ptr(),
);
return false;
}
if cmd.is_empty() {
JS_ReportErrorUTF8(cx, c"Bun.spawn() requires a non-empty command".as_ptr());
return false;
}
let has_opts = opts_val.is_object();
let cwd = if has_opts {
rooted!(&in(cx_ref) let o = opts_val.to_object());
get_string_prop(cx, o.handle().into(), c"cwd".as_ptr())
} else {
None
};
// Bun stdio defaults (docs): stdin "null" (provide no input), stdout
// "pipe", stderr "pipe". A piped stdin default used to leave the write
// end held open forever, so any child reading stdin stalled until
// teardown and 'close' never fired.
let stdio_of = |name: &str, default_piped: bool| -> ::std::process::Stdio {
unsafe {
if !has_opts {
return if default_piped {
::std::process::Stdio::piped()
} else {
::std::process::Stdio::null()
};
}
rooted!(&in(cx_ref) let o = opts_val.to_object());
let name_c = ZBox::from_bytes(name.as_bytes());
let mode = get_string_prop(cx, o.handle().into(), name_c.as_ptr());
match mode.as_deref() {
Some("pipe") => ::std::process::Stdio::piped(),
Some("inherit") => ::std::process::Stdio::inherit(),
Some("null") | Some("ignore") => ::std::process::Stdio::null(),
// Absent → the documented default; unknown string → pipe
// (explicit-but-unrecognized stays closer to "piped" than to
// silently discarding output the caller asked to configure).
_ => if default_piped {
::std::process::Stdio::piped()
} else {
::std::process::Stdio::null()
},
}
}
};
let stdin_mode = stdio_of("stdin", false);
let stdout_mode = stdio_of("stdout", true);
let stderr_mode = stdio_of("stderr", true);
let mut command = ::std::process::Command::new(&cmd);
for arg in &cmd_args {
command.arg(arg);
}
if let Some(ref dir) = cwd {
command.current_dir(dir);
}
// env: explicit env object replaces the environment when parseable.
if has_opts {
rooted!(&in(cx_ref) let o = opts_val.to_object());
if let Some(env) = get_env_prop(cx, o.handle().into()) {
command.env_clear();
for (k, v) in env {
command.env(k, v);
}
}
}
command.stdin(stdin_mode);
command.stdout(stdout_mode);
command.stderr(stderr_mode);
match command.spawn() {
Ok(mut child) => {
let pid = child.id() as i32;
// Hand the piped stdout/stderr read ends to the shared cp-poll
// thread (drain-before-publish): output of any size is captured
// without a 64KB pipe-buffer deadlock, and the exit info is
// published only after both pipes hit EOF.
// posix: raw fds drive the pump thread (register_async_child).
// windows: the async pump face is the child_process domain's
// windows arm (handles, not fds) — until it lands, the pipes stay
// owned by the Child (pushed to SPAWNED_PROCS below for the
// process lifetime) and the child registers exit-only. REGISTERED
// GAP: piped output is not drained on windows yet; a child that
// fills its pipe blocks (visible, not silent corruption) — the
// pump faces land with the child_process windows wave.
#[cfg(unix)]
use ::std::os::fd::AsRawFd;
#[cfg(unix)]
let stdout_pipe = child.stdout.take();
#[cfg(unix)]
let stderr_pipe = child.stderr.take();
#[cfg(unix)]
let stdout_fd = stdout_pipe.as_ref().map(|s| s.as_raw_fd()).unwrap_or(-1);
#[cfg(unix)]
let stderr_fd = stderr_pipe.as_ref().map(|s| s.as_raw_fd()).unwrap_or(-1);
#[cfg(windows)]
let stdout_fd: i32 = -1;
#[cfg(windows)]
let stderr_fd: i32 = -1;
let piped_out = stdout_fd >= 0;
let piped_err = stderr_fd >= 0;
// ChildStdout/ChildStderr close their fd on drop; ownership
// transfers to the pump thread (which closes at EOF) — forget
// the wrappers so the fds stay open.
#[cfg(unix)]
if let Some(s) = stdout_pipe {
::std::mem::forget(s);
}
#[cfg(unix)]
if let Some(s) = stderr_pipe {
::std::mem::forget(s);
}
#[cfg(windows)]
let (stdout_pipe, stderr_pipe): (
Option<::std::process::ChildStdout>,
Option<::std::process::ChildStderr>,
) = (None, None); // pipes stay owned by the Child (process lifetime)
// windows arm (#18): exit-only registration — null pipe ends and
// no `*mut Process` oracle (std::process::Child, not the spawn
// face) ⇒ no poll thread. The registered drain gap above stands
// until the Bun.spawn windows stdio rework lands.
#[cfg(windows)]
let registered = crate::node_child_process::register_async_child(
pid,
::std::ptr::null_mut(),
::std::ptr::null_mut(),
::std::ptr::null_mut(),
None,
);
#[cfg(unix)]
let registered =
crate::node_child_process::register_async_child(pid, stdout_fd, stderr_fd, -1);
if !registered {
// Pump thread failed to start (fail-closed): without it the
// pipes never drain — the child would block on a full pipe
// and 'close' would never fire. Kill it, close our fds, and
// surface a real error instead of a silent hang.
if stdout_fd >= 0 {
unsafe {
libc::close(stdout_fd);
}
}
if stderr_fd >= 0 {
unsafe {
libc::close(stderr_fd);
}
}
#[cfg(windows)]
let _killed = plat::kill(pid, plat::SIGKILL);
#[cfg(not(windows))]
let _killed = unsafe { libc::kill(pid, libc::SIGKILL) } == 0;
let _ = _killed;
JS_ReportErrorUTF8(
cx,
c"Bun.spawn(): failed to start the output pump thread".as_ptr(),
);
return false;
}
// The Child handle stays alive here for the process lifetime
// (ownership of the pid; reaping belongs to the pump thread).
SPAWNED_PROCS.with(|p| p.borrow_mut().push(Box::new(child)));
rooted!(&in(cx_ref) let subproc_obj = JS_NewPlainObject(cx_ref));
if subproc_obj.get().is_null() {
args.rval().set(UndefinedValue());
return true;
}
let subproc_h = subproc_obj.handle().into();
let define_int = |cx: *mut JSContext, name: *const ::std::os::raw::c_char, v: i32| {
unsafe {
rooted!(&in(cx_ref) let rv = Int32Value(v));
JS_DefineProperty(cx, subproc_h, name, rv.handle().into(), JSPROP_ENUMERATE as u32);
}
};
define_int(cx, c"pid".as_ptr(), pid);
define_int(cx, c"exitCode".as_ptr(), -1);
define_int(cx, c"_pid".as_ptr(), pid as i32);
define_int(cx, c"_pipedOut".as_ptr(), if piped_out { 1 } else { 0 });
define_int(cx, c"_pipedErr".as_ptr(), if piped_err { 1 } else { 0 });
{
rooted!(&in(cx_ref) let kv = BooleanValue(false));
JS_DefineProperty(
cx,
subproc_h,
c"killed".as_ptr(),
kv.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
JS_DefineFunction(
cx_ref,
subproc_obj.handle(),
c"wait".as_ptr(),
::std::option::Option::Some(subproc_wait),
0,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx_ref,
subproc_obj.handle(),
c"kill".as_ptr(),
::std::option::Option::Some(subproc_kill),
1,
JSPROP_ENUMERATE as u32,
);
// Non-blocking exit probe for the event-surface watcher in the
// wrapper below: null while running, [exitCode, signal] once the
// child is reaped AND its pipes are drained.
JS_DefineFunction(
cx_ref,
subproc_obj.handle(),
c"_pollExitInfo".as_ptr(),
::std::option::Option::Some(subproc_poll_exit_info),
0,
0,
);
JS_DefineFunction(
cx_ref,
subproc_obj.handle(),
c"_stdoutText".as_ptr(),
::std::option::Option::Some(subproc_stdout_text),
0,
0,
);
JS_DefineFunction(
cx_ref,
subproc_obj.handle(),
c"_stdoutRaw".as_ptr(),
::std::option::Option::Some(subproc_stdout_raw),
0,
0,
);
JS_DefineFunction(
cx_ref,
subproc_obj.handle(),
c"_stderrText".as_ptr(),
::std::option::Option::Some(subproc_stderr_text),
0,
0,
);
JS_DefineFunction(
cx_ref,
subproc_obj.handle(),
c"_stderrRaw".as_ptr(),
::std::option::Option::Some(subproc_stderr_raw),
0,
0,
);
// Register the JS finish callback with the native drain pump
// (spawn_watch_pump_all) — exit/close delivery without any
// JS-timer chain.
JS_DefineFunction(
cx_ref,
subproc_obj.handle(),
c"_watchExit".as_ptr(),
::std::option::Option::Some(subproc_watch_exit),
1,
0,
);
// @trace REQ-ENG-005 [api:Bun.spawn asyncDispose + stdout/stderr/exited]
// — Upstream tests (buffer-copy-fill-detach.test.ts, buffer.test.js)
// use the modern await-using pattern:
// await using proc = Bun.spawn({...});
// const [stdout, stderr, exitCode] = await Promise.all([
// proc.stdout.text(), proc.stderr.text(), proc.exited,
// ]);
// The proc object exposes:
// - `exited`: a Promise<number> resolved by the exit watcher
// (never blocks the JS thread — the old accessor called the
// blocking native wait() on first read).
// - `stdout` / `stderr`: stream-like objects whose `text()`
// resolves the captured output (UTF-8, TextDecoder
// replacement) and whose 'data' events deliver the raw bytes
// (binary-safe: NUL bytes survive; invalid UTF-8 maps
// byte-per-code-unit so charCodeAt(i) === byte[i]).
// - 'exit' at process death, stdout/stderr 'data'/'end' at pipe
// EOF, 'close' last — the watcher starts at spawn time, so
// 'close' is guaranteed once the child is gone regardless of
// listener registration (empty stdout included).
// - `Symbol.asyncDispose`: waits for exit, matching the
// AsyncDisposable contract.
let dispose_src = r#"(function(proc) {
if (!proc) return proc;
var _exitResolve = null;
var _exitedPromise = new Promise(function(resolve) { _exitResolve = resolve; });
Object.defineProperty(proc, 'exited', {
configurable: true,
enumerable: true,
get: function() { return _exitedPromise; },
});
// ── EventEmitter surface (child_process.spawn parity) ──────────────────
var _events = {};
function _arr(ev) { return _events[ev] || (_events[ev] = []); }
proc.on = function(ev, cb) {
if (typeof cb !== 'function') return proc;
_arr(ev).push(cb);
return proc;
};
proc.once = function(ev, cb) {
var g = function() { proc.off(ev, g); cb.apply(null, arguments); };
g.listener = cb;
return proc.on(ev, g);
};
proc.off = function(ev, cb) {
var a = _events[ev];
if (a) {
var i = a.indexOf(cb);
if (i >= 0) a.splice(i, 1);
}
return proc;
};
proc.emit = function(ev) {
var a = _events[ev];
if (!a || a.length === 0) return false;
a = a.slice();
var args = Array.prototype.slice.call(arguments, 1);
for (var i = 0; i < a.length; i++) {
try { a[i].apply(null, args); } catch (e) {}
}
return true;
};
// Exit completion — driven by the NATIVE drain pump: the wrapper registers
// `finish` via proc._watchExit; spawn_watch_pump_all (called on every
// drain, the same integration point as the cluster pump) invokes it once
// the child is reaped AND its pipes drained. No JS-timer chain: the first
// cut's chained-setTimeout watcher spontaneously died at a ~1/300 rate
// (timer popped with its gc_store callback missing → close never fired).
var _finished = false;
function _finish(code, signal) {
if (_finished) return;
_finished = true;
proc.exitCode = code;
try { _exitResolve(code); } catch (e) {}
try { proc.emit('exit', code, signal); } catch (e) {}
if (proc._pipedOut) {
try {
var out = (typeof proc._stdoutRaw === 'function') ? proc._stdoutRaw() : null;
if (out !== null && out !== undefined) proc.emit('stdout_data', out);
proc.emit('stdout_end');
} catch (e) {}
}
if (proc._pipedErr) {
try {
var err = (typeof proc._stderrRaw === 'function') ? proc._stderrRaw() : null;
if (err !== null && err !== undefined) proc.emit('stderr_data', err);
proc.emit('stderr_end');
} catch (e) {}
}
try { proc.emit('close', code, signal); } catch (e) {}
}
if (typeof proc._watchExit === 'function') {
proc._watchExit(_finish);
}
// stdout / stderr stream-like wrappers with text() and data/end events.
function makeStream(textFn, dataEv, endEv) {
return {
text: function() {
var s = (typeof textFn === 'function') ? (textFn.call(proc) || '') : '';
if (s && typeof s.then === 'function') return s;
return Promise.resolve(String(s));
},
on: function(ev, cb) {
return proc.on(ev === 'data' ? dataEv : (ev === 'end' ? endEv : ev), cb);
},
read: function() { return Promise.resolve(null); },
};
}
var _stdoutStream = makeStream(proc._stdoutText, 'stdout_data', 'stdout_end');
var _stderrStream = makeStream(proc._stderrText, 'stderr_data', 'stderr_end');
Object.defineProperty(proc, 'stdout', {
configurable: true, enumerable: true,
get: function() { return _stdoutStream; },
});
Object.defineProperty(proc, 'stderr', {
configurable: true, enumerable: true,
get: function() { return _stderrStream; },
});
// Symbol.asyncDispose — invoked at end of `await using proc { ... }`.
// Contract (TC39 Explicit Resource Management): returns a Promise; we
// resolve it when the child has exited (the pump-backed watcher reaps).
if (typeof Symbol === 'function' && Symbol.asyncDispose) {
proc[Symbol.asyncDispose] = function() {
return Promise.resolve(proc.exited);
};
}
return proc;
})"#;
let dispose_filename = ZBox::from_bytes("bun:spawn-dispose".as_bytes());
let dispose_opts = mozjs::glue::NewCompileOptions(cx, dispose_filename.as_ptr(), 1);
if !dispose_opts.is_null() {
let mut dispose_text = mozjs::rust::transform_str_to_source_text(dispose_src);
let mut dispose_rval = UndefinedValue();
let dispose_rval_h = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut dispose_rval,
};
mozjs_sys::jsapi::JS::Evaluate2(
cx,
dispose_opts,
&mut dispose_text,
dispose_rval_h,
);
mozjs::glue::DeleteCompileOptions(dispose_opts);
if dispose_rval.is_object() {
rooted!(&in(cx_ref) let wrapper_fn = dispose_rval.to_object());
// Call wrapper(subproc_obj) — pass proc as `this` and arg.
rooted!(&in(cx_ref) let proc_val_elem = ObjectValue(subproc_obj.get()));
let args_arr = HandleValueArray {
length_: 1,
elements_: &*proc_val_elem.handle(),
};
let mut call_rval = UndefinedValue();
let call_rval_h = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut call_rval,
};
rooted!(&in(cx_ref) let wrapper_val = ObjectValue(wrapper_fn.get()));
let _ = mozjs_sys::jsapi::JS_CallFunctionValue(
cx,
subproc_obj.handle().into(),
wrapper_val.handle().into(),
&args_arr,
call_rval_h,
);
}
}
args.rval().set(ObjectValue(subproc_obj.get()));
true
}
Err(e) => {
let msg = format!("Bun.spawn() failed: {}", e);
let c_msg = ZBox::from_bytes(msg.as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
false
}
}
}
/// Read the `_pid` int32 property off a Bun.spawn proc object. The pid is
/// the registry key for the child's CP_ASYNC_STATES entry (pump thread +
/// captured output + exit info).
unsafe fn get_pid_from_this(cx: *mut JSContext, args: &CallArgs) -> Option<i32> {
unsafe {
let this = args.thisv();
if !this.is_object() {
return None;
}
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj = this.get().to_object());
let mut pid_val = UndefinedValue();
JS_GetProperty(
cx,
obj.handle().into(),
c"_pid".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut pid_val,
},
);
if pid_val.is_int32() {
let pid = pid_val.to_int32();
if pid > 0 {
return Some(pid);
}
}
None
}
}
/// Set an int32 property on `this` (used to publish exitCode from the
/// natives so the JS surface observes it before the events dispatch).
unsafe fn set_int_prop_on_this(
cx: *mut JSContext,
args: &CallArgs,
name: *const ::std::os::raw::c_char,
v: i32,
) {
unsafe {
let this = args.thisv();
if !this.is_object() {
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 obj = this.to_object());
rooted!(&in(cx_ref) let v_root = Int32Value(v));
JS_SetProperty(cx, obj.handle().into(), name, v_root.handle().into());
}
}
/// `proc.wait()` — block until the pump publishes exit info (reaped + pipes
/// drained), then return the exit code (signal deaths → -1). The pump thread
/// owns reaping, so this must NOT touch Child::wait (its waitpid would steal
/// the status out from under the pump).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn subproc_wait(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let Some(pid) = get_pid_from_this(cx, &args) else {
args.rval().set(Int32Value(-1));
return true;
};
// Bounded-frequency poll of the published exit info. The pump always
// publishes (drain-before-publish invariant), so this terminates when
// the child does.
let info = loop {
if let Some(info) = crate::node_child_process::poll_exit_info(pid) {
break info;
}
::std::thread::sleep(::std::time::Duration::from_millis(2));
};
set_int_prop_on_this(cx, &args, c"exitCode".as_ptr(), info.0);
args.rval().set(Int32Value(info.0));
true
}
/// `proc._pollExitInfo()` — non-blocking probe for the event-surface
/// watcher: `null` while the child runs or its pipes are still draining,
/// `[exitCode, signal]` once published. Also mirrors the exit code onto the
/// `exitCode` property.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn subproc_poll_exit_info(
cx: *mut JSContext,
_argc: u32,
vp: *mut JSVal,
) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let Some(pid) = get_pid_from_this(cx, &args) else {
args.rval().set(NullValue());
return true;
};
let Some((code, signal)) = crate::node_child_process::poll_exit_info(pid) else {
args.rval().set(NullValue());
return true;
};
set_int_prop_on_this(cx, &args, c"exitCode".as_ptr(), code);
let vals = [Int32Value(code), Int32Value(signal)];
let hva = HandleValueArray {
length_: vals.len(),
elements_: vals.as_ptr(),
};
// SAFETY: `hva` points at the rooted-by-scope local `vals` for the
// duration of this single engine call.
let arr = unsafe { NewArrayObject(cx, &hva) };
args.rval().set(if arr.is_null() {
NullValue()
} else {
ObjectValue(arr)
});
true
}
/// `proc.kill(signal?)` — Bun contract: default SIGTERM; accepts a signal
/// name string or number; returns whether the signal was sent. Never marks
/// the proc exited — exit state comes from the pump (the old version set
/// `exited=true` here, which lied about a child that was still dying).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn subproc_kill(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let Some(pid) = get_pid_from_this(cx, &args) else {
args.rval().set(BooleanValue(false));
return true;
};
let sig: i32 = if argc > 0 {
let v = *args.get(0).ptr;
if v.is_int32() {
v.to_int32()
} else if v.is_string() {
let name = crate::js_to_rust_string(cx, v).to_uppercase();
match name.as_str() {
"SIGHUP" => {
#[cfg(windows)]
let sig = plat::SIGHUP;
#[cfg(not(windows))]
let sig = libc::SIGHUP as i32;
sig
}
"SIGINT" => libc::SIGINT as i32,
"SIGQUIT" => {
#[cfg(windows)]
let sig = plat::SIGQUIT;
#[cfg(not(windows))]
let sig = libc::SIGQUIT as i32;
sig
}
"SIGABRT" => libc::SIGABRT as i32,
"SIGKILL" => {
#[cfg(windows)]
let sig = plat::SIGKILL;
#[cfg(not(windows))]
let sig = libc::SIGKILL as i32;
sig
}
"SIGUSR1" => {
#[cfg(windows)]
let sig = plat::SIGUSR1;
#[cfg(not(windows))]
let sig = libc::SIGUSR1 as i32;
sig
}
"SIGUSR2" => {
#[cfg(windows)]
let sig = plat::SIGUSR2;
#[cfg(not(windows))]
let sig = libc::SIGUSR2 as i32;
sig
}
"SIGTERM" => libc::SIGTERM as i32,
_ => {
let msg = format!("Bun.spawn kill(): unknown signal name");
let c_msg = ZBox::from_bytes(msg.as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
return false;
}
}
} else {
libc::SIGTERM as i32
}
} else {
libc::SIGTERM as i32
};
// SAFETY: pid > 0 and sig came from libc constants or a caller-supplied
// i32 — the kernel validates both operands.
#[cfg(windows)]
let sent = plat::kill(pid, sig);
#[cfg(not(windows))]
let sent = unsafe { libc::kill(pid, sig) } == 0;
if sent {
let this = args.thisv();
if this.is_object() {
let mut wrapped_cx_k =
mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref_k = &mut wrapped_cx_k;
rooted!(&in(cx_ref_k) let obj = this.to_object());
rooted!(&in(cx_ref_k) let killed_root = BooleanValue(true));
JS_SetProperty(
cx,
obj.handle().into(),
c"killed".as_ptr(),
killed_root.handle().into(),
);
}
}
args.rval().set(BooleanValue(sent));
true
}
/// Shared body for the four capture readers (stdout/stderr × text/raw).
/// `stdout` selects the pipe; `lossy_text` selects the decoding (see
/// `js_string_from_child_bytes`). Returns null only when the pid is not in
/// the registry (never for a registered child — empty capture is "").
unsafe fn subproc_output_common(
cx: *mut JSContext,
args: &CallArgs,
stdout: bool,
lossy_text: bool,
) -> bool {
unsafe {
let Some(pid) = get_pid_from_this(cx, args) else {
args.rval().set(NullValue());
return true;
};
let Some(bytes) = crate::node_child_process::peek_output(pid, stdout) else {
args.rval().set(NullValue());
return true;
};
let js_str = js_string_from_child_bytes(cx, &bytes, lossy_text);
args.rval().set(if js_str.is_null() {
NullValue()
} else {
StringValue(&*js_str)
});
true
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn subproc_stdout_text(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
subproc_output_common(cx, &args, true, true)
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn subproc_stdout_raw(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
subproc_output_common(cx, &args, true, false)
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn subproc_stderr_text(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
subproc_output_common(cx, &args, false, true)
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn subproc_stderr_raw(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
subproc_output_common(cx, &args, false, false)
}
unsafe fn get_string_prop(
cx: *mut JSContext,
obj_h: Handle<*mut JSObject>,
name: *const ::std::os::raw::c_char,
) -> Option<String> {
unsafe {
let mut val = UndefinedValue();
let mh = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut val,
};
JS_GetProperty(cx, obj_h, name, mh);
if val.is_string() {
Some(crate::js_to_rust_string(cx, val))
} else {
None
}
}
}
unsafe fn get_string_array_prop(
cx: *mut JSContext,
obj_h: Handle<*mut JSObject>,
name: *const ::std::os::raw::c_char,
) -> Vec<String> {
unsafe {
let mut val = UndefinedValue();
let mh = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut val,
};
JS_GetProperty(cx, obj_h, name, mh);
if !val.is_object() {
return Vec::new();
}
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let arr = val.to_object());
read_string_array_from_obj(cx, arr.handle().into())
}
}
unsafe fn get_env_prop(
cx: *mut JSContext,
obj_h: Handle<*mut JSObject>,
) -> Option<Vec<(String, String)>> {
unsafe {
let mut val = UndefinedValue();
let mh = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut val,
};
JS_GetProperty(cx, obj_h, c"env".as_ptr(), mh);
if !val.is_object() {
return None;
}
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let env_obj = val.to_object());
let mut ids_ptr: *mut JSString = ::std::ptr::null_mut();
let _ids_mh = MutableHandle::<*mut JSString> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut ids_ptr,
};
if !JS_GetProperty(
cx,
env_obj.handle().into(),
c"__envKeys__".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut val,
},
) {
return None;
}
None
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn stdin_read(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let mut buf = [0u8; 4096];
match ::std::io::stdin().lock().read(&mut buf) {
Ok(0) => {
args.rval().set(NullValue());
}
Ok(n) => {
let s = ::std::str::from_utf8(&buf[..n]).unwrap_or("");
let js_str = js_string_from_utf8(cx, s);
if !js_str.is_null() {
args.rval().set(StringValue(&*js_str));
} else {
args.rval().set(NullValue());
}
}
Err(_) => {
args.rval().set(NullValue());
}
}
true
}
thread_local! {
static STDIN_LISTENER_KEYS: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
}
/// Global counter for generating unique GcStore keys for stdin listener callbacks.
static STDIN_CB_COUNTER: AtomicU64 = AtomicU64::new(0);
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn stdin_on(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc < 2 {
args.rval().set(UndefinedValue());
return true;
}
let event_val = *args.get(0).ptr;
let fn_val = *args.get(1).ptr;
if !event_val.is_string() || !fn_val.is_object() {
args.rval().set(UndefinedValue());
return true;
}
let event = jsstr_to_string(&mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx)), NonNull::new_unchecked(event_val.to_string()));
if event != "data" && event != "end" && event != "close" && event != "error" {
args.rval().set(UndefinedValue());
return true;
}
let mut wrapped_cx_on = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref_on = &mut wrapped_cx_on;
rooted!(&in(cx_ref_on) let callback = fn_val.to_object());
let cb_id = STDIN_CB_COUNTER.fetch_add(1, Ordering::Relaxed);
let key = format!("stdin_cb_{}", cb_id);
gc_store_insert(cx, &key, callback.get());
STDIN_LISTENER_KEYS.with(|l| {
l.borrow_mut().push(key);
});
args.rval().set(UndefinedValue());
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn stdin_pipe(_cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let this = *args.thisv().ptr;
args.rval().set(this);
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn stdin_resume(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let this = args.thisv();
if !this.is_object() {
args.rval().set(UndefinedValue());
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 this_obj = this.to_object());
let readable_v = BooleanValue(true);
let rv_h = Handle::<JSVal> {
_phantom_0: ::std::marker::PhantomData,
ptr: &readable_v,
};
JS_DefineProperty(
cx,
this_obj.handle().into(),
c"readable".as_ptr(),
rv_h,
JSPROP_ENUMERATE as u32,
);
args.rval().set(ObjectValue(this_obj.get()));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn stdin_pause(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let this = args.thisv();
if !this.is_object() {
args.rval().set(UndefinedValue());
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 this_obj = this.to_object());
let readable_v = BooleanValue(false);
let rv_h = Handle::<JSVal> {
_phantom_0: ::std::marker::PhantomData,
ptr: &readable_v,
};
JS_DefineProperty(
cx,
this_obj.handle().into(),
c"readable".as_ptr(),
rv_h,
JSPROP_ENUMERATE as u32,
);
args.rval().set(ObjectValue(this_obj.get()));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn stdin_destroy(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let this = args.thisv();
if !this.is_object() {
args.rval().set(UndefinedValue());
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 this_obj = this.to_object());
let destroyed_v = BooleanValue(true);
let dv_h = Handle::<JSVal> {
_phantom_0: ::std::marker::PhantomData,
ptr: &destroyed_v,
};
JS_DefineProperty(
cx,
this_obj.handle().into(),
c"destroyed".as_ptr(),
dv_h,
JSPROP_ENUMERATE as u32,
);
args.rval().set(ObjectValue(this_obj.get()));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_serve(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
// @trace REQ-ENG-006 [api:Bun.serve] HTTP server via bun_uws::App (C++ uWS)
// @trace REQ-SEC-001 [api:POST /fetch] Bun.serve builds a raw HTTP server
// via bun_uws::App that serves any origin directly — there is no CORS
// preflight (OPTIONS) handling, no cors_check(), no Origin enforcement,
// and no opaque responses. Inbound HTTP is unrestricted, matching the
// REQ-SEC-001 "disable web security" requirement that bao's HTTP surface
// never blocks cross-origin requests.
let args = CallArgs::from_vp(vp, argc);
let mut port: u16 = 3000;
let mut hostname = "0.0.0.0".to_string();
let mut fetch_handler: Option<*mut JSObject> = None;
let mut websocket_handler: Option<*mut JSObject> = None;
if argc > 0 {
let opts_val = *args.get(0).ptr;
if opts_val.is_object() {
let mut wrapped_cx_opts =
mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref_opts = &mut wrapped_cx_opts;
rooted!(&in(cx_ref_opts) let opts_obj = opts_val.to_object());
let opts_h = opts_obj.handle().into();
let mut port_val = UndefinedValue();
let _ = bao_stealth::engine_props::get_property_clearing(cx, opts_h, c"port", &mut port_val);
if port_val.is_int32() {
port = port_val.to_int32().max(0) as u16;
} else if port_val.is_double() {
port = port_val.to_double().max(0.0) as u16;
}
let mut hn_val = UndefinedValue();
let _ = bao_stealth::engine_props::get_property_clearing(cx, opts_h, c"hostname", &mut hn_val);
if hn_val.is_string() {
hostname = crate::js_to_rust_string(cx, hn_val);
}
let mut fetch_val = UndefinedValue();
let _ = bao_stealth::engine_props::get_property_clearing(cx, opts_h, c"fetch", &mut fetch_val);
if fetch_val.is_object() {
rooted!(&in(cx_ref_opts) let fetch_obj = fetch_val.to_object());
if JS_ObjectIsFunction(fetch_obj.get()) {
fetch_handler = Some(fetch_obj.get());
}
}
// REQ-ENG-006 criterion 5: WebSocket handler object. Upstream
// Bun.serve takes the BEHAVIOR OBJECT — `websocket: { open,
// message, close, … }` — and ws_on_open/message/close resolve
// those methods off the stored object (GcStore). Audit: the old
// detection required a FUNCTION here, so the object form never
// registered the app.ws() route and upgrades fell through to the
// fetch/426 path (no handshake). Any object is accepted; a
// function would carry no .open/.message methods and never fire.
let mut ws_val = UndefinedValue();
let _ = bao_stealth::engine_props::get_property_clearing(cx, opts_h, c"websocket", &mut ws_val);
if ws_val.is_object() {
rooted!(&in(cx_ref_opts) let ws_obj = ws_val.to_object());
websocket_handler = Some(ws_obj.get());
}
}
}
// Store callbacks in GcStore for GC safety
let serve_id = SERVE_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
let fetch_cb_key = fetch_handler.map(|cb| {
let key = format!("serve_fetch_{}", serve_id);
gc_store_insert(cx, &key, cb);
key
});
let websocket_cb_key = websocket_handler.map(|cb| {
let key = format!("serve_ws_{}", serve_id);
gc_store_insert(cx, &key, cb);
key
});
// Clone keys for JS property definition (keys will be moved into BunServeUserData)
let fetch_cb_key_for_js = fetch_cb_key.clone();
let websocket_cb_key_for_js = websocket_cb_key.clone();
// Ensure MiniEventLoop is initialized (drain_and_check will tick it).
crate::timers::with_event_loop(|_| {});
// Create uWS App (C++ HTTP server). Gracefully degrade when uSockets
// backend is unavailable (stub mode) — JS API contract is preserved.
// Note: HttpFlags::isNodeHttp stays at its default `false` — Bun.serve
// semantics: RFC 9112 6.1 rejects an HTTP/1.0 request bearing
// Transfer-Encoding with 400 (node_http::server_listen opts into llhttp
// parity via set_is_node_http(true)).
let opts = BunSocketContextOptions::default();
let app_ptr = App::<false>::create(&opts).unwrap_or(::std::ptr::null_mut());
// BCE-007 (runtime hang): register the App with the unified JS-thread
// liveness registry so `drain_and_check` keeps ticking the uWS Loop while
// this server is listening. Without this, the server's listen socket never
// receives `accept()` events and inbound connections (e.g. `fetch(self)`)
// hang in EINPROGRESS forever. Matches `node_http::server_listen` which
// pushes to the same registry. Idempotent + null-safe.
// @trace REQ-ENG-006 [api:Bun.serve] unified liveness registration
// Safety: app_ptr is a live `*mut App<false>` from `App::create` (or null,
// which register_active_app handles).
unsafe {
crate::node_http::register_active_app(app_ptr);
}
// Store fetch_handler + websocket_handler in user_data for the route callback
let ud = Box::new(BunServeUserData {
fetch_cb_key,
websocket_cb_key: websocket_cb_key.clone(),
app_ptr: app_ptr as *mut ::std::ffi::c_void,
hostname: hostname.clone(),
port,
actual_port: AtomicU16::new(0),
// @trace REQ-ENG-006 [api:Bun.serve fetch handler] JSContext* bound to
// this server — read back by `bun_serve_route_handler` to call the
// user fetch callback. Matches node_http::ServerUserData::cx pattern.
cx,
});
let ud_ptr = Box::into_raw(ud) as *mut ::std::ffi::c_void;
// Register catch-all route
//
// @trace REQ-ENG-006 [api:Bun.serve request body] [level:design]
// Two-phase dispatch with a LIVE REQUEST BODY STREAM (upstream Bun
// semantics: the fetch handler runs at HEADERS-complete and the body
// streams in). uWS fires the route handler the moment the request HEADERS
// are parsed; the request BODY chunks arrive afterwards through
// `res.on_data` (uWS HttpContext.h: the body-chunk callback runs after
// the route handler returns — first for the remainder of the triggering
// socket read, then once more with an empty `last=true` chunk). Dispatch
// is split:
// phase 1 (route handler): capture the header set (the uWS Request is
// only valid for this call), register on_aborted + on_data, then
// build the Request with a per-request controller-driven
// ReadableStream body (the web_streams.js face — the native side
// feeds `controller.enqueue` through the parked bundle object) and
// invoke the fetch handler AT HEADERS-COMPLETE. The handler's return
// value is parked in GcStore under a per-request key. The route
// handler never writes to the wire — a mid-body response would break
// uWS keep-alive framing (an unread body would be misparsed as the
// start of the next request), so every write is deferred to
// body-complete.
// phase 2 (on_data): each chunk is enqueued into the stream controller
// AS IT ARRIVES (per-chunk delivery — no buffering); on the final
// empty `last=true` chunk (always delivered — bodyless requests
// included) the stream is closed, the parked handler value resolved
// to a Response, and the response written.
//
// Teardown ownership (exactly-once free of the state Box):
// - on_aborted sets the latch, errors the body stream (WHATWG
// network-error shape: pending readers reject, never hang), drops the
// parked faces, and frees the state only when no dispatch borrow is
// live (phase == CAPTURED). A connection dying mid-spin
// (phase == DISPATCHING) leaves the free to the spin, which observes
// the latch within one iteration — the dispatch borrow's AtomicBool
// stays valid for the whole spin.
// - phase 2 clears the abort latch BEFORE freeing (a written response
// never fires the latch — same discipline as node_http2's
// h2_on_aborted).
//
// Body cap: 128 MB (the upstream BodyReaderMixin MAX_BODY_SIZE), tracked
// as a byte counter (the stream path never buffers). Overflow errors the
// stream (a pending text()/getReader() rejects — never a silently
// truncated body) and answers 413 at body-complete.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_serve_route_handler(
res: *mut bun_uws_sys::response::c::uws_res,
req: *mut Request,
user_data: *mut ::std::ffi::c_void,
) {
let ud = &*(user_data as *const BunServeUserData);
let res_mut = Response::<false>::cast_res(res);
let req_ref = bun_opaque::opaque_deref_mut(req);
// Phase 1 — capture everything the uWS Request can tell us before it
// dies at handler return; the rest of the dispatch works exclusively
// off these owned copies.
let method = req_ref.method().to_vec();
let url = req_ref.url().to_vec();
let mut header_pairs: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
req_ref.for_each_header(
|pairs: &mut Vec<(Vec<u8>, Vec<u8>)>, name: &[u8], value: &[u8]| {
pairs.push((name.to_vec(), value.to_vec()));
},
&mut header_pairs as *mut Vec<(Vec<u8>, Vec<u8>)>,
);
let req_id = SERVE_REQUEST_COUNTER.fetch_add(1, Ordering::Relaxed);
let state = Box::new(ServeRequestState {
ud,
method,
url,
headers: header_pairs,
req_id,
body_seen: 0,
body_too_large: false,
outcome: ServePhase1Outcome::Dispatch,
phase: AtomicU8::new(SERVE_PHASE_CAPTURED),
aborted: AtomicBool::new(false),
});
let state_ptr = Box::into_raw(state);
// Abort latch: ZST closure, state flows through the heap pointer (the
// latch lives in the state so both callbacks see one object).
(*res_mut).on_aborted(
|st: *mut ServeRequestState, _res: &mut Response<false>| {
(*st).aborted.store(true, Ordering::Release);
if (*st).phase.load(Ordering::Acquire) == SERVE_PHASE_CAPTURED {
// No dispatch borrow is live — the abort path owns the
// teardown. (Mid-spin aborts must NOT free: the spin
// holds the borrow and frees on latch observation.) The
// body stream may still be open — error it so pending
// readers reject with a network error instead of hanging
// on a dead connection.
serve_body_stream_signal(st, ServeBodySignal::AbortError);
serve_drop_body_faces(st);
drop(Box::from_raw(st));
}
},
state_ptr,
);
// Body chunks. uWS always delivers a final empty `last=true` chunk
// once on_data is registered (bodyless requests included), so phase 2
// completes exactly once per request.
(*res_mut).on_data(
|st: *mut ServeRequestState, res: &mut Response<false>, chunk: &[u8], last: bool| {
// Cap accounting: a byte counter, not a buffer (the stream
// path never accumulates). Crossing the cap errors the stream
// immediately — a pending text()/getReader() rejects instead
// of observing a truncated body.
if !(*st).body_too_large {
(*st).body_seen = (*st).body_seen.saturating_add(chunk.len());
if (*st).body_seen > SERVE_BODY_MAX {
(*st).body_too_large = true;
serve_body_stream_signal(st, ServeBodySignal::CapError);
}
}
if !(*st).body_too_large && !chunk.is_empty() {
serve_body_stream_enqueue(st, chunk);
}
if !last {
return;
}
if (*st)
.phase
.compare_exchange(
SERVE_PHASE_CAPTURED,
SERVE_PHASE_DISPATCHING,
Ordering::AcqRel,
Ordering::Acquire,
)
.is_err()
{
// Double dispatch — impossible per the uWS contract (the
// inStream handler is cleared after the fin chunk); never
// free a state whose ownership is contested.
return;
}
if (*st).aborted.load(Ordering::Acquire) {
// Connection died before the body completed. Never touch
// the dead res; the free is ours (the abort handler saw
// phase == DISPATCHING and left the state for us).
SERVE_ABORT_COUNT.fetch_add(1, Ordering::Relaxed);
serve_drop_body_faces(st);
drop(Box::from_raw(st));
return;
}
if (*st).body_too_large {
(*res).write_status(b"413 Payload Too Large");
(*res).write_header(b"Content-Type", b"text/plain");
(*res).end(b"request body exceeds the 128MB serve body cap", true);
(*res).clear_aborted();
serve_drop_body_faces(st);
drop(Box::from_raw(st));
return;
}
// Body complete: close the stream (queued chunks then done —
// the deferred-close drain is the web_streams.js contract),
// resolve the parked handler value, write the response.
serve_body_stream_signal(st, ServeBodySignal::Close);
serve_finish_and_respond(res, st);
// serve_finish_and_respond owns the response write, the
// face drop, and the clear_aborted-before-free teardown on
// every path.
},
state_ptr,
);
// Headers-complete dispatch: build the streaming Request and invoke
// the fetch handler NOW. Registered callbacks above already satisfy
// uWS' "respond or register onAborted" contract; this call only marks
// the state's outcome — all wire writes stay at body-complete.
serve_dispatch_headers(state_ptr);
}
let safe_handler: Option<
extern "C" fn(
*mut bun_uws_sys::response::c::uws_res,
*mut Request,
*mut ::std::ffi::c_void,
),
> = unsafe {
::std::mem::transmute(Some(
bun_serve_route_handler
as unsafe extern "C" fn(
*mut bun_uws_sys::response::c::uws_res,
*mut Request,
*mut ::std::ffi::c_void,
),
))
};
if !app_ptr.is_null() {
// @trace REQ-ENG-006 [api:Bun.serve WebSocket] Register `app.ws()` route
// BEFORE `app.any()` so uWS routes WebSocket upgrade requests to the WS
// handler and regular HTTP requests to the any handler. The `app.ws()`
// pattern "/*" matches all paths for WebSocket upgrades.
if websocket_cb_key.is_some() {
let behavior = ws_build_behavior();
// App::ws(ctx, pattern, id, behavior) — ctx is the user-data pointer
// passed to the upgrade callback (our BunServeUserData). id is an
// arbitrary identifier (not used in our callbacks).
(*app_ptr).ws(b"/*", ud_ptr, 0, behavior);
}
(*app_ptr).any(b"/*", safe_handler, ud_ptr);
// @trace BCE-20260618-005 [level:regression] [api:Bun.serve port]
// Listen callback — captures the actual bound port (for `port: 0`
// dynamic bind) and logs. uWS fires this synchronously inside
// `App::listen` (see uWS App.h:688-690: `handler(trackListenSocket(...))`
// is called before `listen` returns), so `actual_port` is populated
// by the time `bun_serve` reads it below.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_serve_listen_cb(
listen_socket: *mut ListenSocket,
user_data: *mut ::std::ffi::c_void,
) {
if !listen_socket.is_null() {
let ls_ref = bun_opaque::opaque_deref_mut(listen_socket);
let ls_port = ls_ref.get_local_port();
if ls_port > 0 {
if !user_data.is_null() {
let ud = &*(user_data as *const BunServeUserData);
ud.actual_port.store(ls_port as u16, Ordering::Release);
}
log::info!("Bun.serve() listening (uWS port={})", ls_port);
}
}
}
let safe_listen_cb: extern "C" fn(*mut ListenSocket, *mut ::std::ffi::c_void) = unsafe {
::std::mem::transmute(
bun_serve_listen_cb
as unsafe extern "C" fn(*mut ListenSocket, *mut ::std::ffi::c_void),
)
};
(*app_ptr).listen(port as i32, safe_listen_cb, ud_ptr);
log::info!("Bun.serve() listening on {}:{}", hostname, port);
}
// Build JS server object
let mut wrapped_cx = unsafe { mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx)) };
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let server_obj = JS_NewPlainObject(cx_ref));
if server_obj.get().is_null() {
args.rval().set(UndefinedValue());
return true;
}
let srv_h = server_obj.handle().into();
// @trace BCE-20260618-005 — expose the actual bound port on `server.port`.
// For `port: 0` (dynamic bind), `actual_port` was populated by the listen
// callback synchronously inside `App::listen` above. Fall back to the
// requested `port` when no dynamic port was assigned (e.g. uSockets in
// stub mode where the listen callback never fires).
let bound_port = (*(ud_ptr as *const BunServeUserData))
.actual_port
.load(Ordering::Acquire);
let exposed_port = if bound_port > 0 { bound_port } else { port } as i32;
rooted!(&in(cx_ref) let port_root = Int32Value(exposed_port));
JS_DefineProperty(
cx,
srv_h,
c"port".as_ptr(),
port_root.handle().into(),
JSPROP_ENUMERATE as u32,
);
{
let hn_str = js_string_from_utf8(cx, &hostname);
if !hn_str.is_null() {
rooted!(&in(cx_ref) let hn_v = StringValue(&*hn_str));
JS_DefineProperty(
cx,
srv_h,
c"hostname".as_ptr(),
hn_v.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
// Store app_ptr as private property for stop() — use PrivateValue to
// preserve full 64-bit pointer (Int32Value truncates upper 32 bits).
let app_val = mozjs::jsval::PrivateValue(app_ptr as *const core::ffi::c_void);
rooted!(&in(cx_ref) let app_h = app_val);
JS_DefineProperty(cx, srv_h, c"_appPtr".as_ptr(), app_h.handle().into(), 0);
// Store GcStore keys on the server object for cleanup in stop()
if let Some(ref fk) = fetch_cb_key_for_js {
let c_fk = ZBox::from_bytes(fk.as_bytes());
{
let fk_str = JS_NewStringCopyZ(cx, c_fk.as_ptr());
if !fk_str.is_null() {
rooted!(&in(cx_ref) let v = StringValue(&*fk_str));
JS_DefineProperty(cx, srv_h, c"_fetchCbKey".as_ptr(), v.handle().into(), 0);
}
}
}
if let Some(ref wk) = websocket_cb_key_for_js {
let c_wk = ZBox::from_bytes(wk.as_bytes());
{
let wk_str = JS_NewStringCopyZ(cx, c_wk.as_ptr());
if !wk_str.is_null() {
rooted!(&in(cx_ref) let v = StringValue(&*wk_str));
JS_DefineProperty(cx, srv_h, c"_wsCbKey".as_ptr(), v.handle().into(), 0);
}
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn server_stop(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let mut wrapped_cx_stop = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref_stop = &mut wrapped_cx_stop;
rooted!(&in(cx_ref_stop) let this_obj = args.thisv().to_object());
let this_h = this_obj.handle().into();
let mut app_val = UndefinedValue();
JS_GetProperty(
cx,
this_h,
c"_appPtr".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut app_val,
},
);
// @trace BCE-20260618-002 — guard non-private doubles before to_private().
// server.stop() before serve() actually listens leaves _appPtr undefined
// (PrivateValue) — to_private() on undefined asserts is_double() → panic.
let app_ptr = if app_val.is_double() && (app_val.asBits_ & 0xFFFF000000000000) == 0 {
app_val.to_private() as *mut App<false>
} else {
core::ptr::null_mut()
};
if !app_ptr.is_null() {
// Close listen sockets first, then destroy app.
// Skip destroys socket group with dangling listen sockets → assertion.
(*app_ptr).close();
// BCE-007: unregister BEFORE destroy so `has_active_servers()`
// stops reporting liveness for the now-destroyed App. Matches the
// register call in `bun_serve`; idempotent.
// Safety: app_ptr was registered in bun_serve (or is the null path).
unsafe {
crate::node_http::unregister_active_app(app_ptr);
}
App::<false>::destroy(app_ptr);
log::info!("Bun.serve() stopped");
// @trace BCE-20260618-006 [level:regression] [api:Bun.serve stop]
// Clear `_appPtr` on the JS server object so subsequent `stop()`
// calls are idempotent no-ops instead of use-after-free on the
// destroyed `*mut App`. Without this, a second `server.stop()`
// (common in try/finally cleanup paths and `test_http_depth.js`'s
// `finishTests`) reads the stale pointer and calls `close()` /
// `destroy()` on freed memory → SIGSEGV. Set the slot to a non-
// private value (UndefinedValue) so the BCE-002 private-value
// guard above correctly takes the null path on re-entry.
let undef = UndefinedValue();
let undef_h = Handle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &undef,
};
JS_SetProperty(cx, this_h, c"_appPtr".as_ptr(), undef_h);
}
// Clean up GcStore entries for fetch and websocket callbacks
let mut fk_val = UndefinedValue();
JS_GetProperty(
cx,
this_h,
c"_fetchCbKey".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut fk_val,
},
);
if fk_val.is_string() {
let key = crate::js_to_rust_string(cx, fk_val);
gc_store_remove(cx, &key);
}
let mut wk_val = UndefinedValue();
JS_GetProperty(
cx,
this_h,
c"_wsCbKey".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut wk_val,
},
);
if wk_val.is_string() {
let key = crate::js_to_rust_string(cx, wk_val);
gc_store_remove(cx, &key);
}
args.rval().set(UndefinedValue());
true
}
unsafe extern "C" fn server_ref(_cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
args.rval().set(UndefinedValue());
true
}
unsafe extern "C" fn server_unref(_cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
args.rval().set(UndefinedValue());
true
}
mozjs_sys::jsapi::JS_DefineFunction(
cx,
srv_h,
c"stop".as_ptr(),
Some(server_stop),
0,
JSPROP_ENUMERATE as u32,
);
mozjs_sys::jsapi::JS_DefineFunction(
cx,
srv_h,
c"ref".as_ptr(),
Some(server_ref),
0,
JSPROP_ENUMERATE as u32,
);
mozjs_sys::jsapi::JS_DefineFunction(
cx,
srv_h,
c"unref".as_ptr(),
Some(server_unref),
0,
JSPROP_ENUMERATE as u32,
);
args.rval().set(mozjs::jsval::ObjectValue(server_obj.get()));
true
}
/// User data passed to uWS route handler via bun_uws::App::any.
#[allow(dead_code)]
struct BunServeUserData {
fetch_cb_key: Option<String>,
websocket_cb_key: Option<String>,
app_ptr: *mut ::std::ffi::c_void,
hostname: String,
/// Requested port (what the caller passed to Bun.serve).
port: u16,
/// Actual bound port captured in the listen callback.
/// @trace BCE-20260618-005 — for `port: 0` (dynamic bind) this holds the
/// real OS-assigned port, read back by `bun_serve` to expose on the JS
/// server object. Initialized to 0; set synchronously inside the uWS
/// `listen` callback (which fires before `App::listen` returns — see
/// uWS App.h:688-690). Atomic because the C++ listen callback may run on
/// the uSockets I/O thread.
actual_port: AtomicU16,
/// @trace REQ-ENG-006 [api:Bun.serve fetch handler] [level:design]
/// JSContext* for the bound server — used by `bun_serve_route_handler`
/// to invoke the user-supplied `fetch` JS callback and marshal the
/// returned Response back to the uWS C++ Response. Mirrors the
/// `node_http::ServerUserData::cx` field — same pattern: store the cx
/// at construction time (when `bun_serve` runs on the JS thread), read
/// it back from the route handler. The route handler is always invoked
/// on the JS thread (the uWS App is JS-thread-bound and ticked by
/// `drain_and_check`), so this cx is valid at dispatch time.
cx: *mut JSContext,
}
impl BunServeUserData {
/// Resolve the fetch handler JS function from GcStore. Must be called
/// inside the realm (dispatch sites `AutoRealm` into the persistent realm).
/// @trace REQ-ENG-006 [api:Bun.serve fetch handler]
fn fetch_handler(&self) -> Option<*mut JSObject> {
let key = self.fetch_cb_key.as_ref()?;
if self.cx.is_null() {
return None;
}
gc_store_get(self.cx, key)
}
}
/// Global counter for generating unique GcStore keys for WebSocket per-socket objects.
static WS_SOCKET_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
/// User data passed to uWS WebSocket callbacks via `app.ws()`.
/// Stored as the socket's user-data pointer (set during upgrade via
/// `Response::upgrade()`). Each connected WebSocket gets its own
/// `BunWsUserData` instance.
#[allow(dead_code)]
struct BunWsUserData {
/// GcStore key for the JS WebSocket wrapper object.
ws_obj_key: String,
/// GcStore key for the JS websocket handler object (shared across all
/// sockets on this server — the user's `websocket` option from `Bun.serve`).
ws_handler_key: String,
/// JSContext* bound to this server.
cx: *mut JSContext,
}
// ──────────────────────────────────────────────────────────────────────────
// @trace REQ-ENG-006 [api:Bun.serve WebSocket] Real WebSocket upgrade via
// uWS App::ws(). The upgrade handler creates a JS WebSocket wrapper object,
// calls the user's JS `websocket` handler to accept/reject, and then hands
// the connection over to uWS's native WebSocket protocol engine. Subsequent
// open/message/close/ping callbacks invoke the user's JS handlers.
// ──────────────────────────────────────────────────────────────────────────
/// Build the WebSocketBehavior for Bun.serve's `app.ws()` registration.
/// The behavior wires up VTable callbacks that bridge uWS C callbacks to JS.
fn ws_build_behavior() -> WebSocketBehavior {
WebSocketBehavior {
compression: 0,
max_payload_length: u32::MAX,
idle_timeout: 120,
max_backpressure: 1024 * 1024,
close_on_backpressure_limit: false,
reset_idle_timeout_on_send: true,
send_pings_automatically: true,
max_lifetime: 0,
upgrade: Some(ws_on_upgrade),
open: Some(ws_on_open),
message: Some(ws_on_message),
drain: None,
ping: Some(ws_on_ping),
pong: None,
close: Some(ws_on_close),
}
}
/// Create a JS WebSocket wrapper object with `send(data)`, `close(code, reason)`,
/// `ping(data)`, `terminate()` methods. The uWS RawWebSocket pointer is stored
/// as private properties `_wsPtrHi` / `_wsPtrLo` on the JS object.
///
/// # Safety
/// - `cx` must be a live JSContext on the current thread.
/// - `raw_ws` must be a live `*mut RawWebSocket` (valid for the socket's lifetime).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn ws_create_js_object(cx: *mut JSContext, raw_ws: *mut RawWebSocket) -> *mut JSObject {
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
let raw_cx = cx_ref.raw_cx();
rooted!(&in(cx_ref) let ws_obj = JS_NewPlainObject(cx_ref));
if ws_obj.get().is_null() {
return ::std::ptr::null_mut();
}
// Store the RawWebSocket pointer as two int32 private properties.
let ptr_bits = raw_ws as u64;
let ptr_hi = (ptr_bits >> 32) as i32;
let ptr_lo = (ptr_bits & 0xFFFFFFFF) as i32;
{
let hi = Int32Value(ptr_hi);
rooted!(&in(cx_ref) let hi_r = hi);
JS_DefineProperty(
raw_cx,
ws_obj.handle().into(),
c"_wsPtrHi".as_ptr(),
hi_r.handle().into(),
0,
);
}
{
let lo = Int32Value(ptr_lo);
rooted!(&in(cx_ref) let lo_r = lo);
JS_DefineProperty(
raw_cx,
ws_obj.handle().into(),
c"_wsPtrLo".as_ptr(),
lo_r.handle().into(),
0,
);
}
// ws.send(data) — send text or binary message over the WebSocket.
JS_DefineFunction(
cx_ref,
ws_obj.handle(),
c"send".as_ptr(),
Some(ws_js_send),
1,
JSPROP_ENUMERATE as u32,
);
// ws.close(code, reason) — close the WebSocket with an optional code and reason.
JS_DefineFunction(
cx_ref,
ws_obj.handle(),
c"close".as_ptr(),
Some(ws_js_close),
2,
JSPROP_ENUMERATE as u32,
);
// ws.ping(data) — send a ping frame.
JS_DefineFunction(
cx_ref,
ws_obj.handle(),
c"ping".as_ptr(),
Some(ws_js_ping),
1,
JSPROP_ENUMERATE as u32,
);
// ws.terminate() — immediately terminate the WebSocket connection.
JS_DefineFunction(
cx_ref,
ws_obj.handle(),
c"terminate".as_ptr(),
Some(ws_js_terminate),
0,
JSPROP_ENUMERATE as u32,
);
// readyState: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED
// Set to CONNECTING(0) initially — the socket does not exist yet during upgrade.
// ws_on_open will set it to OPEN(1) once the connection is actually established.
{
let ready_val = Int32Value(0);
rooted!(&in(cx_ref) let rv = ready_val);
JS_DefineProperty(
raw_cx,
ws_obj.handle().into(),
c"readyState".as_ptr(),
rv.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
// bufferedAmount — initial 0.
{
let ba_val = Int32Value(0);
rooted!(&in(cx_ref) let bav = ba_val);
JS_DefineProperty(
raw_cx,
ws_obj.handle().into(),
c"bufferedAmount".as_ptr(),
bav.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
ws_obj.get()
}
/// Extract the RawWebSocket pointer from the JS WebSocket wrapper object's
/// private `_wsPtrHi` / `_wsPtrLo` properties.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn ws_get_raw_ptr(cx: *mut JSContext, obj_h: Handle<*mut JSObject>) -> *mut RawWebSocket {
let mut hi_val = UndefinedValue();
JS_GetProperty(
cx,
obj_h,
c"_wsPtrHi".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut hi_val,
},
);
let mut lo_val = UndefinedValue();
JS_GetProperty(
cx,
obj_h,
c"_wsPtrLo".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut lo_val,
},
);
if hi_val.is_int32() && lo_val.is_int32() {
let hi = (hi_val.to_int32() as u32) as u64;
let lo = (lo_val.to_int32() as u32) as u64;
let ptr = ((hi << 32) | lo) as *mut RawWebSocket;
if !ptr.is_null() {
return ptr;
}
}
::std::ptr::null_mut()
}
/// JS method: ws.send(data) — sends a text or binary message.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn ws_js_send(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let this = args.thisv();
if !this.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 this_obj = this.to_object());
let raw_ws = ws_get_raw_ptr(cx, this_obj.handle().into());
if raw_ws.is_null() {
args.rval().set(BooleanValue(false));
return true;
}
let data_val = if argc > 0 {
*args.get(0).ptr
} else {
UndefinedValue()
};
let message: Vec<u8>;
let opcode: Opcode;
if data_val.is_string() {
let s = crate::js_to_rust_string(cx, data_val);
message = s.into_bytes();
opcode = Opcode::Text;
} else if data_val.is_object() {
// Try ArrayBuffer / TypedArray / ArrayBufferView — extract bytes directly.
if let Some(bytes) = extract_bytes_from_jsval(cx, data_val) {
message = bytes;
opcode = Opcode::Binary;
} else {
// Not a binary object — convert to string via JS::ToString and send as text.
// SAFETY: data_val is an object; rooted handle for ToString call.
rooted!(&in(cx_ref) let data_root = data_val);
let jsstr = mozjs::rust::ToString(cx_ref, data_root.handle());
if jsstr.is_null() {
args.rval().set(BooleanValue(false));
return true;
}
let str_val = StringValue(&*jsstr);
message = crate::js_to_rust_string(cx, str_val).into_bytes();
opcode = Opcode::Text;
}
} else {
args.rval().set(BooleanValue(false));
return true;
}
// SAFETY: RawWebSocket and NewWebSocket<0> are layout-compatible ZST opaques.
let ws: &mut NewWebSocket<0> = &mut *raw_ws.cast::<NewWebSocket<0>>();
let status = ws.send(&message, opcode);
args.rval()
.set(BooleanValue(matches!(status, SendStatus::Success)));
true
}
/// JS method: ws.close(code, reason) — close the WebSocket.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn ws_js_close(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let this = args.thisv();
if !this.is_object() {
args.rval().set(UndefinedValue());
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 this_obj = this.to_object());
let raw_ws = ws_get_raw_ptr(cx, this_obj.handle().into());
if raw_ws.is_null() {
args.rval().set(UndefinedValue());
return true;
}
let code: i32 = if argc > 0 {
let code_val = *args.get(0).ptr;
if code_val.is_int32() {
code_val.to_int32()
} else {
1000
}
} else {
1000
};
let reason: Vec<u8> = if argc > 1 {
let reason_val = *args.get(1).ptr;
if reason_val.is_string() {
crate::js_to_rust_string(cx, reason_val).into_bytes()
} else {
Vec::new()
}
} else {
Vec::new()
};
// SAFETY: RawWebSocket and NewWebSocket<0> are layout-compatible ZST opaques.
let ws: &mut NewWebSocket<0> = &mut *raw_ws.cast::<NewWebSocket<0>>();
ws.end(code, &reason);
// Update readyState to CLOSING (2).
let closing_val = Int32Value(2);
rooted!(&in(cx_ref) let cv = closing_val);
JS_SetProperty(
cx,
this_obj.handle().into(),
c"readyState".as_ptr(),
cv.handle().into(),
);
args.rval().set(UndefinedValue());
true
}
/// JS method: ws.ping(data) — send a ping frame.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn ws_js_ping(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let this = args.thisv();
if !this.is_object() {
args.rval().set(UndefinedValue());
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 this_obj = this.to_object());
let raw_ws = ws_get_raw_ptr(cx, this_obj.handle().into());
if raw_ws.is_null() {
args.rval().set(UndefinedValue());
return true;
}
let data: Vec<u8> = if argc > 0 {
let data_val = *args.get(0).ptr;
if data_val.is_string() {
crate::js_to_rust_string(cx, data_val).into_bytes()
} else {
Vec::new()
}
} else {
Vec::new()
};
// SAFETY: RawWebSocket and NewWebSocket<0> are layout-compatible ZST opaques.
let ws: &mut NewWebSocket<0> = &mut *raw_ws.cast::<NewWebSocket<0>>();
ws.send(&data, Opcode::Ping);
args.rval().set(UndefinedValue());
true
}
/// JS method: ws.terminate() — immediately terminate the WebSocket.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn ws_js_terminate(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let this = args.thisv();
if !this.is_object() {
args.rval().set(UndefinedValue());
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 this_obj = this.to_object());
let raw_ws = ws_get_raw_ptr(cx, this_obj.handle().into());
if raw_ws.is_null() {
args.rval().set(UndefinedValue());
return true;
}
// SAFETY: RawWebSocket and NewWebSocket<0> are layout-compatible ZST opaques.
let ws: &mut NewWebSocket<0> = &mut *raw_ws.cast::<NewWebSocket<0>>();
ws.close();
// Update readyState to CLOSED (3) and clear the raw pointer.
let closed_val = Int32Value(3);
rooted!(&in(cx_ref) let cv = closed_val);
JS_SetProperty(
cx,
this_obj.handle().into(),
c"readyState".as_ptr(),
cv.handle().into(),
);
// Zero out the pointer to prevent double-close.
let zero_hi = Int32Value(0);
rooted!(&in(cx_ref) let zh = zero_hi);
JS_SetProperty(
cx,
this_obj.handle().into(),
c"_wsPtrHi".as_ptr(),
zh.handle().into(),
);
let zero_lo = Int32Value(0);
rooted!(&in(cx_ref) let zl = zero_lo);
JS_SetProperty(
cx,
this_obj.handle().into(),
c"_wsPtrLo".as_ptr(),
zl.handle().into(),
);
args.rval().set(UndefinedValue());
true
}
/// uWS upgrade callback — called when a client sends a WebSocket upgrade request.
/// Creates the JS WebSocket wrapper object, stores it in GcStore, and calls
/// `res.upgrade()` to hand the connection over to uWS's WS protocol engine.
/// The `context` argument IS the per-route WebSocketContext created by
/// `app.ws()` — it must be forwarded to `res.upgrade()` (the socket
/// transitions into that context; null would hand the connection to no
/// protocol engine and kill the 101 handshake).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn ws_on_upgrade(
user_data: *mut ::std::ffi::c_void,
res: *mut uws_res,
req: *mut Request,
context: *mut WebSocketUpgradeContext,
_id: usize,
) {
let ud = &*(user_data as *const BunServeUserData);
let cx = ud.cx;
if cx.is_null() {
return;
}
let req_ref = bun_opaque::opaque_deref_mut(req);
let res_mut = Response::<false>::cast_res(res);
// Extract Sec-WebSocket-Key header for the upgrade handshake.
let ws_key = req_ref
.header(b"sec-websocket-key")
.map(|h| h.to_vec())
.unwrap_or_default();
let ws_protocol = req_ref
.header(b"sec-websocket-protocol")
.map(|h| h.to_vec())
.unwrap_or_default();
let ws_extensions = req_ref
.header(b"sec-websocket-extensions")
.map(|h| h.to_vec())
.unwrap_or_default();
// Create the JS WebSocket wrapper object. We use a sentinel RawWebSocket
// (null) here — the actual pointer will be set in ws_on_open (the socket
// doesn't exist until after upgrade). For now, store a placeholder that
// will be updated once the socket is created.
let ws_obj = ws_create_js_object(cx, ::std::ptr::null_mut());
if ws_obj.is_null() {
// Failed to allocate JS object — reject the upgrade.
(*res_mut).write_status(b"500 Internal Server Error");
(*res_mut).end(b"", true);
return;
}
// Store the WS object in GcStore so it survives GC.
let socket_id = WS_SOCKET_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
let ws_obj_key = format!("ws_obj_{}", socket_id);
gc_store_insert(cx, &ws_obj_key, ws_obj);
// Create the per-socket user data that will be attached to the uWS socket.
// The ws_handler_key comes from the parent BunServeUserData — it's the
// GcStore key for the user's websocket handler object.
let ws_handler_key = ud.websocket_cb_key.clone().unwrap_or_default();
let ws_ud = Box::new(BunWsUserData {
ws_obj_key: ws_obj_key.clone(),
ws_handler_key,
cx,
});
let ws_ud_ptr = Box::into_raw(ws_ud);
// Store the ws_obj_key on the JS object so it can clean up GcStore on close.
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let ws_obj_root = ws_obj);
let c_key = ZBox::from_bytes(ws_obj_key.as_bytes());
{
let key_str = JS_NewStringCopyZ(cx, c_key.as_ptr());
if !key_str.is_null() {
rooted!(&in(cx_ref) let kv = StringValue(&*key_str));
JS_DefineProperty(
cx,
ws_obj_root.handle().into(),
c"_wsObjKey".as_ptr(),
kv.handle().into(),
0,
);
}
}
// Perform the actual uWS WebSocket upgrade. This writes the 101
// Switching Protocols handshake (Sec-WebSocket-Accept) and transitions
// the socket into the route's WebSocketContext — which then fires
// on_open. The context argument comes from uWS itself (the same
// WebSocketContext `app.ws()` created for this route).
// SAFETY: res is a valid uws_res handle from uWS; ws_ud_ptr is a live
// heap allocation that will be the socket's user-data for its lifetime.
(*res_mut).upgrade::<BunWsUserData>(
ws_ud_ptr,
&ws_key,
&ws_protocol,
&ws_extensions,
Some(&mut *context),
);
}
/// uWS open callback — called when a WebSocket connection is fully established.
/// Updates the JS WebSocket wrapper with the real RawWebSocket pointer and
/// calls the user's `websocket.open` JS handler.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn ws_on_open(raw_ws: *mut RawWebSocket) {
// Retrieve the per-socket user data (set during upgrade).
let ud_ptr = bun_uws_sys::web_socket::c::uws_ws_get_user_data(0, &mut *raw_ws);
if ud_ptr.is_null() {
return;
}
let ws_ud = &*(ud_ptr as *const BunWsUserData);
let cx = ws_ud.cx;
if cx.is_null() {
return;
}
// Enter the context's persistent realm before touching JS — WS callbacks
// fire from the pump with no realm entered, and the JS WebSocket wrapper
// is stored as a property on this realm's global (GcStore).
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 cx_ref: &mut mozjs::context::JSContext = &mut realm;
// Inside the persistent realm: GcStore resolves the WS wrapper.
let Some(ws_obj) = gc_store_get(cx, &ws_ud.ws_obj_key) else {
return;
};
if ws_obj.is_null() {
return;
}
rooted!(&in(cx_ref) let ws_obj_root = ws_obj);
let ptr_bits = raw_ws as u64;
let ptr_hi = (ptr_bits >> 32) as i32;
let ptr_lo = (ptr_bits & 0xFFFFFFFF) as i32;
rooted!(&in(cx_ref) let hi_val = Int32Value(ptr_hi));
JS_SetProperty(
cx,
ws_obj_root.handle().into(),
c"_wsPtrHi".as_ptr(),
hi_val.handle().into(),
);
rooted!(&in(cx_ref) let lo_val = Int32Value(ptr_lo));
JS_SetProperty(
cx,
ws_obj_root.handle().into(),
c"_wsPtrLo".as_ptr(),
lo_val.handle().into(),
);
// Connection is now established — update readyState to OPEN(1).
rooted!(&in(cx_ref) let open_state = Int32Value(1));
JS_SetProperty(
cx,
ws_obj_root.handle().into(),
c"readyState".as_ptr(),
open_state.handle().into(),
);
// Resolve the user's websocket handler object from GcStore.
// The websocket handler is an object with open/message/close methods.
let Some(ws_handler) = ws_ud.ws_handler() else {
return;
};
if ws_handler.is_null() {
return;
}
// Get the `open` method from the websocket handler.
rooted!(&in(cx_ref) let ws_handler_root = ws_handler);
let mut open_val = UndefinedValue();
let _ = bao_stealth::engine_props::get_property_clearing(
cx,
ws_handler_root.handle().into(),
c"open",
&mut open_val,
);
if !open_val.is_object() {
// No open handler — nothing to call.
return;
}
rooted!(&in(cx_ref) let open_fn = open_val.to_object());
if !JS_ObjectIsFunction(open_fn.get()) {
return;
}
// Call open(ws) with the JS WebSocket wrapper object.
rooted!(&in(cx_ref) let open_fn_val = ObjectValue(open_fn.get()));
rooted!(&in(cx_ref) let ws_arg = ObjectValue(ws_obj_root.get()));
let call_args = HandleValueArray {
length_: 1,
elements_: &*ws_arg.handle(),
};
let mut rval = UndefinedValue();
let rval_h = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut rval,
};
let _ok = JS_CallFunctionValue(
cx,
global_root.handle().into(),
open_fn_val.handle().into(),
&call_args,
rval_h,
);
if !_ok {
// The open handler threw — propagate the thrown value to the error
// route instead of silently clearing it (upstream bun d43ddf309a
// problem-domain fix: the thrown value reaches the error path, it is
// never swallowed). Capture-clear-route contract, same as the client
// ws event dispatch in web_api.rs and the serve body-factory path
// below.
let mut exn = UndefinedValue();
JS_GetPendingException(
cx,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut exn,
},
);
JS_ClearPendingException(cx);
rooted!(&in(cx_ref) let reason_root = exn);
if !exn.is_undefined() {
crate::uncaught::route_uncaught_exception(cx, exn);
}
}
}
/// uWS message callback — called when a WebSocket message is received.
/// Invokes the user's `websocket.message` JS handler.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn ws_on_message(
raw_ws: *mut RawWebSocket,
data: *const u8,
length: usize,
opcode: Opcode,
) {
let ud_ptr = bun_uws_sys::web_socket::c::uws_ws_get_user_data(0, &mut *raw_ws);
if ud_ptr.is_null() {
return;
}
let ws_ud = &*(ud_ptr as *const BunWsUserData);
let cx = ws_ud.cx;
if cx.is_null() {
return;
}
// Enter the context's persistent realm before touching JS — message
// callbacks fire from the pump with no realm entered.
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 cx_ref: &mut mozjs::context::JSContext = &mut realm;
// Inside the persistent realm: GcStore resolves ws_obj + ws_handler.
let Some(ws_obj) = gc_store_get(cx, &ws_ud.ws_obj_key) else {
return;
};
let Some(ws_handler) = ws_ud.ws_handler() else {
return;
};
if ws_obj.is_null() || ws_handler.is_null() {
return;
}
// (Bao, BCE) Root BOTH before any allocation below — the handler
// property read can run a JS getter and the message building allocates
// (string/Uint8Array); a minor GC there moves nursery objects and bare
// pointers held across it go stale (same UB class as the
// stamp_promisify_customs SIGSEGV). `ws_obj` is consumed further down
// (ws_arg) — previously only ws_handler was pinned.
rooted!(&in(cx_ref) let ws_obj_root = ws_obj);
rooted!(&in(cx_ref) let ws_handler_root = ws_handler);
let mut msg_val = UndefinedValue();
let _ = bao_stealth::engine_props::get_property_clearing(
cx,
ws_handler_root.handle().into(),
c"message",
&mut msg_val,
);
if !msg_val.is_object() {
return;
}
rooted!(&in(cx_ref) let msg_fn = msg_val.to_object());
if !JS_ObjectIsFunction(msg_fn.get()) {
return;
}
// Build the message JS value. Text frames → JS string; Binary frames → Uint8Array.
let is_text = opcode.0 == Opcode::Text.0;
let mut msg_arg = UndefinedValue();
if is_text {
// SAFETY: data[..length] is valid for the duration of this callback.
let bytes = ::std::slice::from_raw_parts(data, length);
let text = ::std::str::from_utf8(bytes).unwrap_or("");
let js_str = js_string_from_utf8(cx, text);
if !js_str.is_null() {
msg_arg = StringValue(&*js_str);
}
} else {
// Binary: create a JS string from raw bytes (Bun's ws.message
// receives string for text and Buffer for binary; we use string
// for both as a simplification — matches the current Bun API
// surface where binary messages are also passed as strings).
let bytes = ::std::slice::from_raw_parts(data, length);
let c_data = ZBox::from_vec(bytes.to_vec());
let js_str = JS_NewStringCopyZ(cx, c_data.as_ptr());
if !js_str.is_null() {
msg_arg = StringValue(&*js_str);
}
}
// Call message(ws, messageData).
rooted!(&in(cx_ref) let msg_fn_val = ObjectValue(msg_fn.get()));
rooted!(&in(cx_ref) let ws_arg = ObjectValue(ws_obj_root.get()));
rooted!(&in(cx_ref) let msg_arg_root = msg_arg);
let args = [ws_arg.handle().get(), msg_arg_root.handle().get()];
let call_args = HandleValueArray {
length_: 2,
elements_: args.as_ptr(),
};
let mut rval = UndefinedValue();
let rval_h = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut rval,
};
let _ok = JS_CallFunctionValue(
cx,
global_root.handle().into(),
msg_fn_val.handle().into(),
&call_args,
rval_h,
);
if !_ok {
// The message handler threw — propagate the thrown value to the
// error route instead of silently clearing it (upstream bun
// d43ddf309a problem-domain fix). Capture-clear-route contract, same
// as the client ws event dispatch in web_api.rs and the serve
// body-factory path above.
let mut exn = UndefinedValue();
JS_GetPendingException(
cx,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut exn,
},
);
JS_ClearPendingException(cx);
rooted!(&in(cx_ref) let reason_root = exn);
if !exn.is_undefined() {
crate::uncaught::route_uncaught_exception(cx, exn);
}
}
}
/// uWS close callback — called when a WebSocket connection is closed.
/// Invokes the user's `websocket.close` JS handler and cleans up GcStore.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn ws_on_close(
raw_ws: *mut RawWebSocket,
code: i32,
message: *const u8,
length: usize,
) {
let ud_ptr = bun_uws_sys::web_socket::c::uws_ws_get_user_data(0, &mut *raw_ws);
if ud_ptr.is_null() {
return;
}
let ws_ud = &*(ud_ptr as *const BunWsUserData);
let cx = ws_ud.cx;
if cx.is_null() {
// Still need to free the user data.
let _ = Box::from_raw(ud_ptr as *mut BunWsUserData);
return;
}
// Enter the context's persistent realm before touching JS — close
// callbacks fire from the pump with no realm entered.
let global = match bao_engine::context::thread_realm_global() {
Some(g) if !g.is_null() => g,
_ => {
let _ = Box::from_raw(ud_ptr as *mut BunWsUserData);
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 cx_ref: &mut mozjs::context::JSContext = &mut realm;
// Inside the persistent realm: GcStore resolves the WS wrapper.
let ws_obj = match gc_store_get(cx, &ws_ud.ws_obj_key) {
Some(o) if !o.is_null() => o,
_ => {
let _ = Box::from_raw(ud_ptr as *mut BunWsUserData);
return;
}
};
rooted!(&in(cx_ref) let ws_obj_root = ws_obj);
// Update readyState to CLOSED (3) and clear the raw pointer on the JS object.
let closed_val = Int32Value(3);
rooted!(&in(cx_ref) let cv = closed_val);
JS_SetProperty(
cx,
ws_obj_root.handle().into(),
c"readyState".as_ptr(),
cv.handle().into(),
);
let zero_hi = Int32Value(0);
rooted!(&in(cx_ref) let zh = zero_hi);
JS_SetProperty(
cx,
ws_obj_root.handle().into(),
c"_wsPtrHi".as_ptr(),
zh.handle().into(),
);
let zero_lo = Int32Value(0);
rooted!(&in(cx_ref) let zl = zero_lo);
JS_SetProperty(
cx,
ws_obj_root.handle().into(),
c"_wsPtrLo".as_ptr(),
zl.handle().into(),
);
// Call the user's close handler if available.
let Some(ws_handler) = ws_ud.ws_handler() else {
gc_store_remove(cx, &ws_ud.ws_obj_key);
let _ = Box::from_raw(ud_ptr as *mut BunWsUserData);
return;
};
if !ws_handler.is_null() {
rooted!(&in(cx_ref) let ws_handler_root = ws_handler);
let mut close_val = UndefinedValue();
let _ = bao_stealth::engine_props::get_property_clearing(
cx,
ws_handler_root.handle().into(),
c"close",
&mut close_val,
);
if close_val.is_object() {
rooted!(&in(cx_ref) let close_fn = close_val.to_object());
if JS_ObjectIsFunction(close_fn.get()) {
// Build close code and reason.
let code_arg = Int32Value(code);
let reason_bytes = if !message.is_null() && length > 0 {
::std::slice::from_raw_parts(message, length).to_vec()
} else {
Vec::new()
};
let reason_str = String::from_utf8_lossy(&reason_bytes).into_owned();
let js_reason = js_string_from_utf8(cx, &reason_str);
rooted!(&in(cx_ref) let close_fn_val = ObjectValue(close_fn.get()));
rooted!(&in(cx_ref) let ws_arg = ObjectValue(ws_obj_root.get()));
rooted!(&in(cx_ref) let code_root = code_arg);
let reason_arg = if !js_reason.is_null() {
StringValue(&*js_reason)
} else {
UndefinedValue()
};
rooted!(&in(cx_ref) let reason_root = reason_arg);
let args = [
ws_arg.handle().get(),
code_root.handle().get(),
reason_root.handle().get(),
];
let call_args = HandleValueArray {
length_: 3,
elements_: args.as_ptr(),
};
let mut rval = UndefinedValue();
let rval_h = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut rval,
};
let _ok = JS_CallFunctionValue(
cx,
global_root.handle().into(),
close_fn_val.handle().into(),
&call_args,
rval_h,
);
if !_ok {
// The close handler threw — propagate the thrown value to
// the error route instead of silently clearing it (upstream
// bun d43ddf309a problem-domain fix). Capture-clear-route
// contract, same as the client ws event dispatch in
// web_api.rs and the serve body-factory path above.
// request_exit only latches thread-local flags, so the
// GcStore/user-data cleanup below still runs.
let mut exn = UndefinedValue();
JS_GetPendingException(
cx,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut exn,
},
);
JS_ClearPendingException(cx);
rooted!(&in(cx_ref) let reason_root = exn);
if !exn.is_undefined() {
crate::uncaught::route_uncaught_exception(cx, exn);
}
}
}
}
}
// Clean up GcStore and free the per-socket user data.
gc_store_remove(cx, &ws_ud.ws_obj_key);
let _ = Box::from_raw(ud_ptr as *mut BunWsUserData);
}
/// uWS ping callback — forward ping frames. uWS auto-responds with pong
/// when `send_pings_automatically` is true, so we just log for diagnostics.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn ws_on_ping(_raw_ws: *mut RawWebSocket, _data: *const u8, _length: usize) {
// uWS automatically sends pong responses when send_pings_automatically
// is true. No JS callback needed for ping frames — they are protocol-level.
}
impl BunWsUserData {
/// Resolve the websocket handler JS object from GcStore.
/// This returns the user's `websocket` option object (with open/message/close
/// methods). The ws_handler_key was set during upgrade from the parent
/// BunServeUserData's websocket_cb_key.
fn ws_handler(&self) -> Option<*mut JSObject> {
if self.cx.is_null() {
return None;
}
gc_store_get(self.cx, &self.ws_handler_key)
}
}
// ──────────────────────────────────────────────────────────────────────────
// @trace REQ-ENG-006 [api:Bun.serve fetch handler] [level:design]
// Bun.serve route-handler JS↔uWS marshalling helpers.
//
// These free functions are invoked synchronously from
// `bun_serve_route_handler` (which runs on the JS thread inside
// `MiniEventLoop::tick_without_idle` dispatched by `drain_and_check`).
// They build a JS Request object from the uWS Request, invoke the user's
// `fetch` JS callback, resolve any returned Promise<Response> by draining
// microtasks + pending fetches on this same thread, and finally marshal
// the Response (status / headers / body) back to the uWS C++ Response.
//
// Mirrors `node_http::uws_route_handler` (the createServer path) for the
// Request build, plus `fetch_api::build_response_object` for the inverse
// Response→uWS direction.
// ──────────────────────────────────────────────────────────────────────────
/// Maximum iterations of the Promise-resolution spin loop before giving up.
/// Each iteration runs microtasks (RunJobs), drains pending async fetches,
/// and ticks the uWS Loop without blocking. The loop bound prevents an
/// infinite hang if a handler never settles (defensive — well-behaved JS
/// settles within a few iterations).
const SERVE_PROMISE_POLL_MAX_ITERS: u32 = 10_000;
/// Serve request-body cap (the upstream BodyReaderMixin MAX_BODY_SIZE). A
/// body that crosses the cap answers 413 instead of silently truncating.
const SERVE_BODY_MAX: usize = 128 * 1024 * 1024;
/// `ServeRequestState` lifecycle phases (guards the exactly-once state free —
/// see the teardown discipline on `bun_serve_route_handler`).
const SERVE_PHASE_CAPTURED: u8 = 1;
const SERVE_PHASE_DISPATCHING: u8 = 2;
/// Monotonic per-request id — roots the per-request GcStore keys (the body
/// stream bundle + the parked handler return value). Unique per request, so
/// concurrent/keep-alive requests never collide and teardown can drop its
/// own faces without touching a neighbour's.
static SERVE_REQUEST_COUNTER: AtomicU64 = AtomicU64::new(1);
/// Which terminal transition to drive on the per-request body stream.
#[derive(Clone, Copy, PartialEq, Eq)]
enum ServeBodySignal {
/// Body complete — `controller.close()` (queued chunks drain, then done).
Close,
/// Connection aborted mid-body — `controller.error(abortError)`: pending
/// readers reject with a TypeError (WHATWG network-error shape) instead
/// of hanging on a dead socket.
AbortError,
/// Body crossed `SERVE_BODY_MAX` — `controller.error(capError)`: a
/// pending text()/getReader() rejects; a truncated body is never
/// delivered.
CapError,
}
/// Phase-1 (headers-complete) dispatch outcome, consumed at body-complete.
enum ServePhase1Outcome {
/// The fetch handler ran at headers-complete. Its return value is parked
/// under the per-request rval GcStore key (key absent at phase 2 = the
/// handler returned a non-object → 404, the pre-existing semantics).
Dispatch,
/// Phase-1 dispatch failure: the deferred status+text write at
/// body-complete. Wire timing matches the two-phase discipline — the
/// answer lands after the body is drained (uWS keep-alive framing).
DeferredFail {
status: &'static str,
body: &'static str,
},
/// The reflective default response (no fetch handler registered, or the
/// Request/stream construction failed) — the pre-existing no-handler
/// behavior, byte-identical timing.
DeferredDefault,
}
/// Per-request serve dispatch state, shared between the uWS route callback
/// (phase 1: capture + headers-complete dispatch) and the body-chunk callback
/// (phase 2: stream feed + completion). Owned by neither callback alone —
/// freed exactly once per the phase discipline documented on
/// `bun_serve_route_handler`.
struct ServeRequestState {
/// Server-level user data (cx / handler key / port fallback). Lives for
/// the server's lifetime, so a borrowed copy cannot dangle.
ud: *const BunServeUserData,
/// Captured header set — the uWS Request dies when the route handler
/// returns, so the rest of the dispatch works exclusively off these
/// owned copies.
method: Vec<u8>,
url: Vec<u8>,
headers: Vec<(Vec<u8>, Vec<u8>)>,
/// Per-request id for the GcStore face keys (`serve_body_{id}` /
/// `serve_rval_{id}`).
req_id: u64,
/// Total request-body bytes observed (cap accounting — no buffering).
body_seen: usize,
/// Set when the body crossed `SERVE_BODY_MAX` — phase 2 answers 413 and
/// the stream has already been errored (pending readers reject).
body_too_large: bool,
/// Phase-1 dispatch outcome (see `ServePhase1Outcome`).
outcome: ServePhase1Outcome,
/// CAPTURED → DISPATCHING; guards the exactly-once free.
phase: AtomicU8,
/// on_aborted latch (folded in so both callbacks share one object).
aborted: AtomicBool,
}
impl ServeRequestState {
/// GcStore key rooting the per-request stream bundle
/// `{stream, controller, abortError, capError}`.
fn body_key(&self) -> String {
format!("serve_body_{}", self.req_id)
}
/// GcStore key rooting the fetch handler's return value (the Response or
/// Promise<Response> awaiting body-complete dispatch).
fn rval_key(&self) -> String {
format!("serve_rval_{}", self.req_id)
}
}
/// Run `work` inside the persistent realm (first-principles realm model: one
/// realm per JsContext, held for the context's lifetime). Serve callbacks
/// fire with NO realm entered; every GcStore op and JS call needs the realm
/// (GcStore roots objects as properties on `CurrentGlobalOrNull`), so every
/// JS-touching serve path funnels through here. Returns `None` when the
/// thread has no live realm (runtime teardown in progress) — the `work`
/// closure never ran, and the caller owns any teardown it would have done.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn serve_with_dispatch_realm<T>(
cx: *mut JSContext,
work: impl FnOnce(&mut mozjs::context::JSContext) -> T,
) -> Option<T> {
let global = bao_engine::context::thread_realm_global()?;
if global.is_null() {
return None;
}
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_ref: &mut mozjs::context::JSContext = &mut realm;
Some(work(realm_ref))
}
/// Capture + clear + route a pending exception left by a failed JS call on a
/// serve dispatch path (never leak past a native entry point — same contract
/// as timers.rs fire_callback).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn serve_route_pending_exception(raw_cx: *mut JSContext) {
let mut exn = UndefinedValue();
JS_GetPendingException(
raw_cx,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut exn,
},
);
JS_ClearPendingException(raw_cx);
if !exn.is_undefined() {
crate::uncaught::route_uncaught_exception(raw_cx, exn);
}
}
/// The per-request body-stream factory source: builds ONE ReadableStream
/// (the web_streams.js face) whose `start(controller)` captures the
/// controller synchronously inside the constructor, plus the two terminal
/// error values the native side hands to `controller.error`. Compiled once
/// per process and cached in GcStore (same discipline as the request
/// builder); called once per request.
///
/// The controller is a plain JS object — the native side cannot call its
/// methods directly, so it drives `controller.enqueue/close/error` through
/// this parked bundle (per-request GcStore key, GC-rooted on the global —
/// no raw JSObject pointers cross callback boundaries).
const SERVE_BODY_STREAM_FACTORY_SRC: &str = r#"(function() {
var ctl;
var stream = new ReadableStream({
start: function(c) { ctl = c; }
});
return {
stream: stream,
controller: ctl,
abortError: new TypeError('network error: connection aborted before the request body completed'),
capError: new RangeError('request body exceeds the 128MB serve body cap')
};
})"#;
/// Cached-factory lookup-or-compile (realm must be entered). Returns null on
/// failure (exception routed; never left pending).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn serve_body_stream_factory(raw_cx: *mut JSContext) -> *mut JSObject {
if let Some(f) = gc_store_get(raw_cx, "serve_body_stream_factory") {
if !f.is_null() {
return f;
}
}
let mut src = mozjs::rust::transform_str_to_source_text(SERVE_BODY_STREAM_FACTORY_SRC);
let opts = mozjs::glue::NewCompileOptions(raw_cx, c"<serve-body-stream-factory>".as_ptr(), 1);
if opts.is_null() {
return ::std::ptr::null_mut();
}
let mut fn_rval = UndefinedValue();
let evaluated = mozjs_sys::jsapi::JS::Evaluate2(
raw_cx,
opts,
&mut src,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut fn_rval,
},
);
mozjs::glue::DeleteCompileOptions(opts);
if !evaluated || !fn_rval.is_object() {
serve_route_pending_exception(raw_cx);
return ::std::ptr::null_mut();
}
let f = fn_rval.to_object();
gc_store_insert(raw_cx, "serve_body_stream_factory", f);
f
}
/// Resolve the per-request body bundle's controller (realm must be entered).
/// Returns null when the bundle is absent (a phase-1 failure outcome never
/// parked one) or malformed.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn serve_body_bundle_controller(
cx_ref: &mut mozjs::context::JSContext,
body_key: &str,
) -> (*mut JSObject, *mut JSObject) {
let raw_cx = cx_ref.raw_cx();
let bundle = match gc_store_get(raw_cx, body_key) {
Some(b) if !b.is_null() => b,
_ => return (::std::ptr::null_mut(), ::std::ptr::null_mut()),
};
rooted!(&in(cx_ref) let bundle_r = bundle);
let mut ctl_val = UndefinedValue();
let got = JS_GetProperty(
raw_cx,
bundle_r.handle().into(),
c"controller".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut ctl_val,
},
);
if !got || !ctl_val.is_object() {
return (::std::ptr::null_mut(), bundle_r.get());
}
rooted!(&in(cx_ref) let ctl_r = ctl_val.to_object());
(ctl_r.get(), bundle_r.get())
}
/// Drive a terminal transition on the per-request body stream (`Close` at
/// body-complete; `AbortError`/`CapError` on the failure paths). A missing
/// bundle (failure outcomes never created one) or a missing realm (runtime
/// teardown) skips the JS face — the connection is going away either way.
///
/// Exceptions from the controller call are captured and routed (never left
/// pending); native sequencing already excludes the closed/errored-controller
/// races, so a throw here is a sequencing bug, not a request condition.
///
/// # Safety
/// `st` must be a live `*mut ServeRequestState` (no dispatch borrow needed —
/// only the cx and the GcStore key are read).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn serve_body_stream_signal(st: *mut ServeRequestState, signal: ServeBodySignal) {
let cx = (*(*st).ud).cx;
if cx.is_null() {
return;
}
let body_key = (*st).body_key();
let entered = serve_with_dispatch_realm(cx, |cx_ref| {
let raw_cx = cx_ref.raw_cx();
let (ctl, bundle) = serve_body_bundle_controller(cx_ref, &body_key);
if ctl.is_null() || bundle.is_null() {
return;
}
rooted!(&in(cx_ref) let ctl_r = ctl);
rooted!(&in(cx_ref) let bundle_r = bundle);
let (method_name, err_prop): (&[u8], &[u8]) = match signal {
ServeBodySignal::Close => (b"close", b""),
ServeBodySignal::AbortError => (b"error", b"abortError"),
ServeBodySignal::CapError => (b"error", b"capError"),
};
let mut method_val = UndefinedValue();
let method_c = ZBox::from_bytes(method_name);
let got = JS_GetProperty(
raw_cx,
ctl_r.handle().into(),
method_c.as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut method_val,
},
);
if !got || !method_val.is_object() {
return;
}
rooted!(&in(cx_ref) let method_fn = method_val.to_object());
// Argument: the terminal error value for error(); controller.close()
// declares no parameters and ignores the extra undefined.
let mut arg_val = UndefinedValue();
if !err_prop.is_empty() {
let err_c = ZBox::from_bytes(err_prop);
let got = JS_GetProperty(
raw_cx,
bundle_r.handle().into(),
err_c.as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut arg_val,
},
);
if !got || !arg_val.is_object() {
return;
}
}
rooted!(&in(cx_ref) let arg_r = arg_val);
let args = HandleValueArray {
length_: 1,
elements_: &*arg_r.handle(),
};
rooted!(&in(cx_ref) let method_fn_val = ObjectValue(method_fn.get()));
let mut rv = UndefinedValue();
let ok = JS_CallFunctionValue(
raw_cx,
ctl_r.handle().into(),
method_fn_val.handle().into(),
&args,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut rv,
},
);
if !ok {
serve_route_pending_exception(raw_cx);
}
});
let _ = entered;
}
/// Enqueue one wire chunk into the per-request body stream. The uWS chunk
/// buffer dies at callback return, so the bytes are copied into a fresh
/// Uint8Array (binary-exact). Per-chunk delivery: each on_data call becomes
/// exactly one `controller.enqueue` — no coalescing, no buffering.
///
/// A missing realm (runtime teardown) skips the enqueue; the state teardown
/// still completes through the phase-2 paths.
///
/// # Safety
/// `st` must be a live `*mut ServeRequestState` in `SERVE_PHASE_CAPTURED`,
/// and `chunk` must be valid for the duration of the call (the uWS callback
/// contract).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn serve_body_stream_enqueue(st: *mut ServeRequestState, chunk: &[u8]) {
let cx = (*(*st).ud).cx;
if cx.is_null() {
return;
}
let body_key = (*st).body_key();
let entered = serve_with_dispatch_realm(cx, |cx_ref| {
let raw_cx = cx_ref.raw_cx();
let (ctl, _bundle) = serve_body_bundle_controller(cx_ref, &body_key);
if ctl.is_null() {
return;
}
rooted!(&in(cx_ref) let ctl_r = ctl);
let mut enq_val = UndefinedValue();
let got = JS_GetProperty(
raw_cx,
ctl_r.handle().into(),
c"enqueue".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut enq_val,
},
);
if !got || !enq_val.is_object() {
return;
}
rooted!(&in(cx_ref) let enq_fn = enq_val.to_object());
// Copy the chunk into a fresh Uint8Array (binary-exact — the uWS
// buffer is only valid for this callback).
rooted!(&in(cx_ref) let arr = JS_NewUint8Array(raw_cx, chunk.len()));
if arr.get().is_null() {
return;
}
if !chunk.is_empty() {
let mut ta_len: usize = 0;
let mut shared = false;
let mut data: *mut u8 = ::std::ptr::null_mut();
let unwrapped =
JS_GetObjectAsUint8Array(arr.get(), &mut ta_len, &mut shared, &mut data);
if unwrapped.is_null() || data.is_null() || ta_len < chunk.len() {
return;
}
::std::ptr::copy_nonoverlapping(chunk.as_ptr(), data, chunk.len());
}
rooted!(&in(cx_ref) let arg_v = ObjectValue(arr.get()));
let args = HandleValueArray {
length_: 1,
elements_: &*arg_v.handle(),
};
rooted!(&in(cx_ref) let enq_fn_val = ObjectValue(enq_fn.get()));
let mut rv = UndefinedValue();
let ok = JS_CallFunctionValue(
raw_cx,
ctl_r.handle().into(),
enq_fn_val.handle().into(),
&args,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut rv,
},
);
if !ok {
serve_route_pending_exception(raw_cx);
}
});
let _ = entered;
}
/// Drop the per-request GcStore faces (stream bundle + parked handler value).
/// EVERY terminal path calls this exactly once before freeing the state —
/// the roots live as global properties, so skipping the drop would leak one
/// bundle + one handler value per request for the server's lifetime. A
/// missing realm (runtime teardown) leaves the untracked properties to the
/// dying global.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn serve_drop_body_faces(st: *mut ServeRequestState) {
let cx = (*(*st).ud).cx;
if cx.is_null() {
return;
}
let body_key = (*st).body_key();
let rval_key = (*st).rval_key();
let entered = serve_with_dispatch_realm(cx, |cx_ref| {
let raw_cx = cx_ref.raw_cx();
gc_store_remove(raw_cx, &body_key);
gc_store_remove(raw_cx, &rval_key);
});
let _ = entered;
}
/// Phase-1 serve dispatch (headers-complete): classify the deferred outcome,
/// then — for a dispatched request — build the Request with a LIVE
/// controller-driven ReadableStream body and invoke the fetch handler NOW
/// (upstream Bun semantics: the handler runs before the body streams in; its
/// returned value is parked for phase 2). No wire writes happen here: every
/// response is deferred to body-complete (phase 2) so an unread body can
/// never be misparsed as the start of the next request on a keep-alive
/// connection.
///
/// Failure paths only MARK the state's outcome ([`ServePhase1Outcome`]) —
/// the write + teardown stay in phase 2, keeping the
/// respond-after-body-drained discipline byte-identical to the two-phase
/// dispatch this replaces.
///
/// # Safety
/// `st` must be a live `*mut ServeRequestState` in `SERVE_PHASE_CAPTURED`
/// whose freeing ownership still belongs to the phase-2 callbacks.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn serve_dispatch_headers(st: *mut ServeRequestState) {
let state = &mut *st;
let ud = &*state.ud;
// REQ-ENG-006 criterion 5: WebSocket upgrade requests are handled by
// the `app.ws()` route registered before this `app.any()` route. If a
// WS upgrade reaches here, no ws route was registered (no websocket
// handler). Upstream Bun semantics: the upgrade request is delivered to
// the FETCH handler in that case (the handler sees the request with
// `Upgrade: websocket` and decides — typically answering 4xx or
// upgrading manually). Only when there is no fetch handler either do we
// answer 426 ourselves. A proper WebSocket handshake requires BOTH
// "Upgrade: websocket" AND "Sec-WebSocket-Key" headers (RFC 6455 §4.1).
// Checking only Upgrade would misclassify non-WS requests that happen
// to carry an Upgrade header (e.g. HTTP/2 h2c, CONNECT tunnelling).
let header_lookup = |name: &[u8]| -> Option<&[u8]> {
state
.headers
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(name))
.map(|(_, v)| v.as_slice())
};
let upgrade_header = header_lookup(b"upgrade").unwrap_or(b"");
let is_ws_upgrade = upgrade_header.eq_ignore_ascii_case(b"websocket")
&& header_lookup(b"sec-websocket-key").is_some();
if is_ws_upgrade && ud.fetch_cb_key.is_none() {
// Neither handler registered — explicit 426 Upgrade Required.
state.outcome = ServePhase1Outcome::DeferredFail {
status: "426 Upgrade Required",
body: "Upgrade Required: no WebSocket handler registered",
};
return;
}
// The reflective default response is used ONLY when the caller created
// the server with no `fetch` handler.
if ud.fetch_cb_key.is_none() {
state.outcome = ServePhase1Outcome::DeferredDefault;
return;
}
let cx = ud.cx;
if cx.is_null() {
log::error!("[bun:serve] fetch handler registered but cx is null — responding 500");
state.outcome = ServePhase1Outcome::DeferredFail {
status: "500 Internal Server Error",
body: "no JS context",
};
return;
}
// Absolute-URL authority for the Request face: the client's Host header
// (HTTP/1.1 mandatory) wins; Host-less requests fall back to the serve
// config's hostname:port.
let serve_authority = {
let host_header = header_lookup(b"host")
.and_then(|h| ::std::str::from_utf8(h).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
match host_header {
Some(h) => h,
None => {
let bound = ud.actual_port.load(Ordering::Acquire);
let port = if bound > 0 { bound } else { ud.port };
format!("{}:{}", ud.hostname, port)
}
}
};
// Bodyless detection: no Content-Length (or a zero-length one) and no
// Transfer-Encoding — mirrors the pre-streaming contract where a
// zero-byte body surfaced as `req.body === null`. Everything else gets
// the live stream even if no chunks ever arrive (a stream that closes
// immediately reads as empty — the WHATWG shape for an empty body).
let has_body = {
let cl = header_lookup(b"content-length")
.and_then(|v| ::std::str::from_utf8(v).ok())
.and_then(|v| v.trim().parse::<usize>().ok())
.unwrap_or(0);
let te = header_lookup(b"transfer-encoding").is_some();
cl > 0 || te
};
// Owned copies for the JS closure (the phase-2 default-response path
// still reads the originals off the state).
let method = state.method.clone();
let url = state.url.clone();
let headers = state.headers.clone();
let rval_key = state.rval_key();
let body_key = state.body_key();
let outcome = serve_with_dispatch_realm(cx, |cx_ref| -> ServePhase1Outcome {
let raw_cx = cx_ref.raw_cx();
// Inside the persistent realm: GcStore resolves the fetch handler.
// Registered-but-unresolvable is an explicit dispatch failure → 500
// (never a silent default echo that impersonates the handler
// response).
let fetch_handler = match ud.fetch_handler() {
Some(h) if !h.is_null() => h,
_ => {
log::error!(
"[bun:serve] fetch handler registered but unresolvable — responding 500"
);
return ServePhase1Outcome::DeferredFail {
status: "500 Internal Server Error",
body: "fetch handler unavailable",
};
}
};
// The live body stream: one ReadableStream per request WITH a body,
// its controller captured by the factory (web_streams.js
// start(controller) runs synchronously inside the constructor). The
// bundle is parked in GcStore so the on_data/abort callbacks can
// drive enqueue/close/error after this call returns. Bodyless
// requests skip the bundle entirely — the phase-2 Close signal
// no-ops on an absent bundle key.
let body_init: Option<*mut JSObject> = if !has_body {
None
} else {
let factory = serve_body_stream_factory(raw_cx);
if factory.is_null() {
return ServePhase1Outcome::DeferredDefault;
}
rooted!(&in(cx_ref) let factory_r = factory);
rooted!(&in(cx_ref) let factory_val = ObjectValue(factory_r.get()));
let mut bundle_val = UndefinedValue();
let factory_ok = JS_CallFunctionValue(
raw_cx,
factory_r.handle().into(),
factory_val.handle().into(),
&HandleValueArray {
length_: 0,
elements_: ::std::ptr::null(),
},
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut bundle_val,
},
);
if !factory_ok || !bundle_val.is_object() {
if !factory_ok {
serve_route_pending_exception(raw_cx);
}
return ServePhase1Outcome::DeferredDefault;
}
rooted!(&in(cx_ref) let bundle_r = bundle_val.to_object());
gc_store_insert(raw_cx, &body_key, bundle_r.get());
// The bundle's stream object is the Request body init (the Request
// classifier parks it on _bodyStreamSource — the WHATWG
// init.body-as-stream form). Bodyless requests take no body init at
// all (`req.body === null` — the preserved pre-streaming contract).
let mut stream_val = UndefinedValue();
let got = JS_GetProperty(
raw_cx,
bundle_r.handle().into(),
c"stream".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut stream_val,
},
);
if !got || !stream_val.is_object() {
return ServePhase1Outcome::DeferredDefault;
}
rooted!(&in(cx_ref) let stream_r = stream_val.to_object());
Some(stream_r.get())
};
let req_obj = serve_build_request_object(
cx_ref,
&method,
&url,
&headers,
&serve_authority,
body_init,
);
if req_obj.is_null() {
return ServePhase1Outcome::DeferredDefault;
}
rooted!(&in(cx_ref) let req_r = req_obj);
// Invoke the fetch handler AT HEADERS-COMPLETE with the streaming
// Request. A synchronous throw answers 500 at body-complete (same
// wire shape as the two-phase dispatch this replaces); the exception
// is cleared, never left pending.
rooted!(&in(cx_ref) let handler_val = ObjectValue(fetch_handler));
rooted!(&in(cx_ref) let req_val_elem = ObjectValue(req_r.get()));
let call_args = HandleValueArray {
length_: 1,
elements_: &*req_val_elem.handle(),
};
let mut rval = UndefinedValue();
let global = CurrentGlobalOrNull(raw_cx);
if global.is_null() {
return ServePhase1Outcome::DeferredFail {
status: "500 Internal Server Error",
body: "no JS realm",
};
}
rooted!(&in(cx_ref) let global_r = global);
let ok = JS_CallFunctionValue(
raw_cx,
global_r.handle().into(),
handler_val.handle().into(),
&call_args,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut rval,
},
);
if !ok {
// JS callback threw — clear pending exception; the 500 lands at
// body-complete.
JS_ClearPendingException(raw_cx);
return ServePhase1Outcome::DeferredFail {
status: "500 Internal Server Error",
body: "fetch handler threw",
};
}
// Park the handler value for phase 2 (objects only: a Response or a
// Promise<Response>). A non-object return stays unparked and phase 2
// answers 404 — the pre-existing semantics.
if rval.is_object() {
gc_store_insert(raw_cx, &rval_key, rval.to_object());
}
ServePhase1Outcome::Dispatch
});
match outcome {
Some(o) => (*st).outcome = o,
None => {
// No realm (runtime teardown in progress) — fail-closed 500 at
// body-complete; nothing JS-reachable remains to dispatch.
log::error!("[bun:serve] no JS realm on this thread — responding 500");
(*st).outcome = ServePhase1Outcome::DeferredFail {
status: "500 Internal Server Error",
body: "no JS realm",
};
}
}
}
/// Phase-2 serve completion: the body has completed (the final empty
/// `last=true` chunk arrived and the stream was closed). Resolve the parked
/// handler value to a Response and write it — or write the deferred phase-1
/// outcome (500/426/default) for requests whose dispatch never started.
///
/// Owns the teardown on every path: drops the per-request GcStore faces and
/// frees the state with `clear_aborted()` BEFORE the free (a written
/// response never fires the latch), and never touches `res` once the latch
/// has fired (the abort handler saw phase == DISPATCHING and left the state
/// free to this function).
///
/// # Safety
/// - `res_mut` must be a live `&mut Response<false>` for the duration of the
/// dispatch (guaranteed by the uWS on_data callback contract).
/// - `st` must be a live `*mut ServeRequestState` in `SERVE_PHASE_DISPATCHING`
/// with the abort latch clear, whose freeing ownership has passed to this
/// call.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn serve_finish_and_respond(res_mut: &mut Response<false>, st: *mut ServeRequestState) {
let state = &mut *st;
let ud = &*state.ud;
let cx = ud.cx;
// Deferred phase-1 outcomes write without touching JS.
match state.outcome {
ServePhase1Outcome::DeferredFail { status, body } => {
(*res_mut).write_status(status.as_bytes());
(*res_mut).write_header(b"Content-Type", b"text/plain");
(*res_mut).end(body.as_bytes(), true);
(*res_mut).clear_aborted();
serve_drop_body_faces(st);
drop(Box::from_raw(st));
return;
}
ServePhase1Outcome::DeferredDefault => {
serve_write_default_response(res_mut, &state.method, &state.url);
(*res_mut).clear_aborted();
serve_drop_body_faces(st);
drop(Box::from_raw(st));
return;
}
ServePhase1Outcome::Dispatch => {}
}
if cx.is_null() {
log::error!("[bun:serve] fetch handler registered but cx is null — responding 500");
(*res_mut).write_status(b"500 Internal Server Error");
(*res_mut).write_header(b"Content-Type", b"text/plain");
(*res_mut).end(b"no JS context", true);
(*res_mut).clear_aborted();
serve_drop_body_faces(st);
drop(Box::from_raw(st));
return;
}
// The parked handler value + the abort latch live across the spin — the
// state borrow keeps both valid for the whole completion.
let aborted_flag = &state.aborted;
let rval_key = state.rval_key();
let completed = serve_with_dispatch_realm(cx, |cx_ref| {
// Absent rval = the handler returned a non-object (undefined/null/…)
// → 404 (Bun semantics: returning nothing from fetch → Not Found).
let resp_obj = match gc_store_get(cx_ref.raw_cx(), &rval_key) {
Some(h) if !h.is_null() => {
rooted!(&in(cx_ref) let handler_r = h);
rooted!(&in(cx_ref) let rval_r = ObjectValue(handler_r.get()));
serve_resolve_response_value(cx_ref, rval_r.handle().get(), aborted_flag)
}
_ => ::std::ptr::null_mut(),
};
if (*st).aborted.load(Ordering::Acquire) {
// Connection aborted mid-dispatch: the latch already fired (it is
// what set the flag, once per request). Abandon the response —
// never write to a dead socket, never touch the dead res (same
// discipline as h2_on_aborted). Record the abandon for the
// regression tests; no clear_aborted (the res is dead).
SERVE_ABORT_COUNT.fetch_add(1, Ordering::Relaxed);
serve_drop_body_faces(st);
drop(Box::from_raw(st));
return;
}
if resp_obj.is_null() {
// Handler returned a non-Response value (undefined/null/etc.) or
// the promise rejected. Default to 404 (Bun semantics: returning
// nothing from fetch → 404 Not Found).
(*res_mut).write_status(b"404 Not Found");
(*res_mut).write_header(b"Content-Type", b"text/plain");
(*res_mut).end(b"Not Found", true);
(*res_mut).clear_aborted();
serve_drop_body_faces(st);
drop(Box::from_raw(st));
return;
}
// Liveness re-check passed above (the flag read precedes this write)
// — the socket is still alive; write the handler's response.
serve_write_response_object(cx, &mut *res_mut, resp_obj);
(*res_mut).clear_aborted();
serve_drop_body_faces(st);
drop(Box::from_raw(st));
});
if completed.is_none() {
// The completion closure never ran (no realm — runtime teardown in
// progress): the teardown it would have done is ours. Fail-closed
// 500 — the parked faces cannot be resolved, and a silent default
// would impersonate the handler.
log::error!("[bun:serve] no JS realm on this thread — responding 500");
(*res_mut).write_status(b"500 Internal Server Error");
(*res_mut).write_header(b"Content-Type", b"text/plain");
(*res_mut).end(b"no JS realm", true);
(*res_mut).clear_aborted();
serve_drop_body_faces(st);
drop(Box::from_raw(st));
}
}
/// Build a JS Request object for a captured serve request.
///
/// Upstream Bun hands serve handlers a REAL `Request` instance:
/// `req instanceof Request` is true, `req.url` is the ABSOLUTE URL
/// ("http://<authority><path><query>" — `new URL(req.url)` works inside a
/// handler), and `req.headers` is a `Headers` instance exposing
/// .get()/.has()/.entries(). The authority comes from the client's Host
/// header (mandatory in HTTP/1.1), falling back to the serve config's
/// hostname:port for Host-less requests.
///
/// The wire method is preserved byte-exact (lowercase "post" stays "post"):
/// the helper bypasses the Request constructor's IANA method normalization,
/// which would uppercase it AND reject arbitrary wire tokens with a
/// TypeError — a serve handler must receive whatever method the client sent.
///
/// Body: for requests that carried one, the per-request controller-driven
/// ReadableStream (the `serve_body_stream_factory` bundle's `stream` object)
/// is handed to the builder as a STREAM init — the Request class parks it on
/// `_bodyStreamSource`, so the WHATWG `body` getter returns that exact live
/// stream (chunks enqueue as the wire delivers them) and `text()` / `json()`
/// / `arrayBuffer()` / `blob()` drain it through the class's ordinary
/// stream-consumption paths. `req.body` is null exactly when the client sent
/// no body.
///
/// # Safety
/// - `cx_ref` must be a live `&mut mozjs::JSContext` on the current thread.
/// - `headers` must be the captured header set (phase-1 copies — the uWS
/// Request is long gone by the time this runs).
/// - `body`, when present, must be the live stream object from the
/// request's own GcStore-rooted bundle.
///
/// Returns a non-null `*mut JSObject` on success, null on failure (the
/// caller falls back to the reflective default response).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn serve_build_request_object(
cx_ref: &mut mozjs::context::JSContext,
method: &[u8],
url: &[u8],
headers: &[(Vec<u8>, Vec<u8>)],
authority: &str,
body: Option<*mut JSObject>,
) -> *mut JSObject {
let raw_cx = cx_ref.raw_cx();
let method_str = ::std::str::from_utf8(method).unwrap_or("GET").to_string();
// url — uWS exposes the request target (path + query) as a relative
// form; the Request face carries the ABSOLUTE URL (upstream Bun), so
// prefix the scheme + authority.
let url_str = ::std::str::from_utf8(url).unwrap_or("/");
let absolute_url = if url_str.starts_with('/') {
format!("http://{}{}", authority, url_str)
} else {
format!("http://{}/{}", authority, url_str)
};
// headers — every captured header the client sent. Built as a plain
// record first; the JS helper consumes it through the Headers
// constructor.
rooted!(&in(cx_ref) let headers_obj = JS_NewPlainObject(cx_ref));
if headers_obj.get().is_null() {
return ::std::ptr::null_mut();
}
for (name, value) in headers {
let c_k = ZBox::from_bytes(name);
// Header values can carry UTF-8 — build the JSString through the
// UTF-8 decoder, not the Latin-1 byte copy.
let js_v = js_string_from_utf8(raw_cx, &String::from_utf8_lossy(value));
if !js_v.is_null() {
let hv = StringValue(&*js_v);
rooted!(&in(cx_ref) let hvr = hv);
JS_DefineProperty(
raw_cx,
headers_obj.handle().into(),
c_k.as_ptr(),
hvr.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
// Body — the live stream goes in as a STREAM init (binary-exact: chunks
// enqueue as the wire delivers them; no UTF-8 lossy decode, no
// buffering). Bodyless requests take no body init.
let body_stream_val = match body {
None => UndefinedValue(),
Some(stream_obj) => {
if stream_obj.is_null() {
return ::std::ptr::null_mut();
}
rooted!(&in(cx_ref) let stream_root = stream_obj);
ObjectValue(stream_root.get())
}
};
rooted!(&in(cx_ref) let body_val = body_stream_val);
// Construct the real Request instance through the global Request class
// (installed by web_fetch_classes::install_fetch_classes) so
// `instanceof Request` and the Headers face are genuine. The tiny
// builder function is compiled once and cached in GcStore (JS-thread
// bound — same lifetime discipline as the realm's fetch handler).
let builder_obj = match gc_store_get(raw_cx, "serve_request_builder") {
Some(o) if !o.is_null() => o,
_ => {
let builder_src = r#"(function(method, url, headers, bodyInit) {
var init = { headers: headers };
if (bodyInit !== undefined) init.body = bodyInit;
var r = new Request(url, init);
r.method = method; // wire-case method preserved (no IANA normalization)
return r;
})"#;
let mut src = mozjs::rust::transform_str_to_source_text(builder_src);
let opts =
mozjs::glue::NewCompileOptions(raw_cx, c"<serve-request-builder>".as_ptr(), 1);
if opts.is_null() {
return ::std::ptr::null_mut();
}
let mut fn_rval = UndefinedValue();
let fn_h = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut fn_rval,
};
let evaluated = mozjs_sys::jsapi::JS::Evaluate2(raw_cx, opts, &mut src, fn_h);
mozjs::glue::DeleteCompileOptions(opts);
if !evaluated || !fn_rval.is_object() {
// Capture, clear, route — a pending exception must never leak
// past a native entry point (same contract as timers.rs
// fire_callback).
let mut exn = UndefinedValue();
JS_GetPendingException(
raw_cx,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut exn,
},
);
JS_ClearPendingException(raw_cx);
if !exn.is_undefined() {
crate::uncaught::route_uncaught_exception(raw_cx, exn);
}
return ::std::ptr::null_mut();
}
let b = fn_rval.to_object();
gc_store_insert(raw_cx, "serve_request_builder", b);
b
}
};
rooted!(&in(cx_ref) let builder_val = ObjectValue(builder_obj));
let m_js = js_string_from_utf8(raw_cx, &method_str);
let u_js = js_string_from_utf8(raw_cx, &absolute_url);
if m_js.is_null() || u_js.is_null() {
return ::std::ptr::null_mut();
}
rooted!(&in(cx_ref) let m_str_r = m_js);
rooted!(&in(cx_ref) let u_str_r = u_js);
rooted!(&in(cx_ref) let m_val = StringValue(&*m_js));
rooted!(&in(cx_ref) let u_val = StringValue(&*u_js));
rooted!(&in(cx_ref) let h_val = ObjectValue(headers_obj.get()));
let call_vals = [
m_val.handle().get(),
u_val.handle().get(),
h_val.handle().get(),
body_val.handle().get(),
];
let call_arr = HandleValueArray {
length_: 4,
elements_: call_vals.as_ptr(),
};
let mut call_rval = UndefinedValue();
rooted!(&in(cx_ref) let null_obj = ::std::ptr::null_mut::<JSObject>());
let called = JS_CallFunctionValue(
raw_cx,
null_obj.handle().into(),
builder_val.handle().into(),
&call_arr,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut call_rval,
},
);
if !called {
// Handler-facing construction failure (e.g. a future Request-class
// change throwing) — capture, clear, route; never leave the
// exception pending on the ScriptThread context.
let mut exn = UndefinedValue();
JS_GetPendingException(
raw_cx,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut exn,
},
);
JS_ClearPendingException(raw_cx);
if !exn.is_undefined() {
crate::uncaught::route_uncaught_exception(raw_cx, exn);
}
return ::std::ptr::null_mut();
}
if !call_rval.is_object() {
return ::std::ptr::null_mut();
}
rooted!(&in(cx_ref) let req_out = call_rval.to_object());
req_out.get()
}
/// Resolve a JS value (the fetch handler's return value) into a Response
/// JSObject, transparently awaiting a `Promise<Response>`.
///
/// If `rval` is already a Response object → return it.
/// If `rval` is a Promise → spin a bounded loop running microtasks + pending
/// fetches + due timers until the promise settles (returning its fulfilled
/// value if a Response object, null otherwise), bailing out early when the
/// connection aborts (the caller distinguishes the abort via `abort_flag`).
/// Otherwise → return null (caller writes 404).
///
/// # Safety
/// - `cx_ref` must be a live `&mut mozjs::JSContext` on the current thread.
/// - `abort_flag` must stay valid for the duration of this call (the serve
/// dispatch state's on_aborted latch — the dispatcher borrow keeps it
/// alive for the whole spin).
/// - Must be called with no other JS-thread code mutating runtime state
/// (the route handler is the sole mutator during dispatch).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn serve_resolve_response_value(
cx_ref: &mut mozjs::context::JSContext,
rval: JSVal,
abort_flag: &AtomicBool,
) -> *mut JSObject {
if !rval.is_object() {
return ::std::ptr::null_mut();
}
rooted!(&in(cx_ref) let obj = rval.to_object());
// Fast path: synchronous Response object.
if !serve_is_promise(cx_ref, obj.get()) {
return if serve_is_response_like(cx_ref, obj.get()) {
obj.get()
} else {
::std::ptr::null_mut()
};
}
// Slow path: Promise<Response>. Drain microtasks + tick MiniEventLoop
// (non-blocking) + fire due timers until the promise settles, the
// connection aborts, or we hit the iteration cap.
// SAFETY: route handler runs on JS thread; all JS that could settle this
// promise also runs on this thread, so RunJobs here is sufficient.
let raw_cx = cx_ref.raw_cx();
let mut iters = 0u32;
loop {
// domain-check a4ed5948b8(own-idiom fix): the connection died
// mid-dispatch — nothing can consume this response anymore. Exit NOW
// instead of burning the JS thread to the cap; the caller sees the
// aborted latch and abandons the write (never write to a dead socket).
if abort_flag.load(Ordering::Acquire) {
return ::std::ptr::null_mut();
}
// Snapshot promise state.
if !JS::IsPromiseObject(obj.handle().into()) {
// Defensive: object lost its promise-ness (shouldn't happen).
return ::std::ptr::null_mut();
}
let state = JS::GetPromiseState(obj.handle().into());
match state {
PromiseState::Fulfilled => {
// Extract the resolution value via the mozjs glue wrapper.
let mut result_val = UndefinedValue();
mozjs::glue::JS_GetPromiseResult(
obj.handle().into(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut result_val,
},
);
if !result_val.is_object() {
return ::std::ptr::null_mut();
}
rooted!(&in(cx_ref) let result_obj = result_val.to_object());
return if serve_is_response_like(cx_ref, result_obj.get()) {
result_obj.get()
} else {
::std::ptr::null_mut()
};
}
PromiseState::Rejected => {
// Promise rejected — clear the pending exception (the rejection)
// and return null. Caller writes 404.
JS_ClearPendingException(raw_cx);
return ::std::ptr::null_mut();
}
_ => {}
}
// Still pending — drive one full event-loop pass. Reuse
// `timers::drain_one_pass` (the pub one-pass driver the bun:test
// runner uses) instead of the former bare RunJobs + tick_without_idle
// pair.
// domain-check a4ed5948b8(own-idiom fix): the former body never
// drained BAO_REGISTRY, so `await new Promise(r => setTimeout(r, 50))`
// inside a handler could NEVER settle inside the spin — it hit
// SERVE_PROMISE_POLL_MAX_ITERS and the caller wrote 404 (the "known
// limitation" this replaces). drain_one_pass performs the identical
// non-blocking `tick_without_idle` (which pops ConcurrentTask
// completions from HTTPThread fetches) AND fires due wall-clock
// timers via drain_bao_timers, then runs microtasks and the pumps.
// The tick nesting depth is unchanged — the former body already
// called tick_without_idle from inside the dispatching tick, and
// drain_one_pass's has_http branch makes the exact same call.
// timers.rs owns this pump domain (the parallel pump fix lands inside
// drain_one_pass); this spin inherits it automatically — nothing
// re-implemented here.
let prev_cx = crate::timers::current_cx();
let fired = crate::timers::drain_one_pass(raw_cx);
// drain_one_pass's CxGuard cleared the thread CURRENT_CX on exit —
// restore the pre-call value exactly: when nested inside
// drain_and_check's dispatch that re-arms the outer pass's
// post-dispatch timer/pump work; when the dispatch came from a bare
// tick (test harness), it restores the null it found.
crate::timers::register_current_cx(prev_cx);
if !fired && crate::timers::has_pending_timers() {
// A bao wall-clock timer is pending but was not due this pass:
// pace the spin at 1ms so its deadline actually arrives instead
// of burning the iteration cap against a frozen wall clock.
// (Fetch-only handlers have no bao timers — no sleep, unchanged
// busy-poll behavior for them.)
::std::thread::sleep(::std::time::Duration::from_millis(1));
}
iters += 1;
if iters >= SERVE_PROMISE_POLL_MAX_ITERS {
// Bounded — give up rather than hang the server forever.
return ::std::ptr::null_mut();
}
}
}
/// Check if a JS object is a Promise (SpiderMonkey internal Promise class).
#[allow(unsafe_op_in_unsafe_fn)]
fn serve_is_promise(cx_ref: &mut mozjs::context::JSContext, obj: *mut JSObject) -> bool {
let mut wrapped_cx =
unsafe { mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx_ref.raw_cx())) };
let cx_r = &mut wrapped_cx;
unsafe {
rooted!(&in(cx_r) let obj_r = obj);
JS::IsPromiseObject(obj_r.handle().into())
}
}
/// Duck-type check: does this object look like a Response (has a numeric
/// `status` and a string `_bodyText` or string `body`)? Bao's
/// `fetch_api::response_constructor` and `build_response_object` produce
/// exactly this shape, so this avoids requiring an `instanceof Response`
/// guard (which would need a rooted constructor lookup).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn serve_is_response_like(
cx_ref: &mut mozjs::context::JSContext,
obj: *mut JSObject,
) -> bool {
let raw_cx = cx_ref.raw_cx();
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(raw_cx));
let cx_r = &mut wrapped_cx;
rooted!(&in(cx_r) let obj_r = obj);
let mut status_val = UndefinedValue();
let _ = bao_stealth::engine_props::get_property_clearing(raw_cx, obj_r.handle().into(), c"status", &mut status_val);
// Response objects always have a numeric `status`. This is sufficient
// to distinguish a Response from a Promise/array/other object.
status_val.is_int32() || status_val.is_double()
}
/// Write a JS Response object back to the uWS Response.
///
/// Reads `status` (default 200), `headers` (object → header lines), and
/// `_bodyText` (body bytes, written binary-safe via ZBox). Mirrors the
/// inverse of `fetch_api::build_response_object`.
///
/// # Safety
/// - `raw_cx` must be a live `JSContext*` on the current thread.
/// - `res_mut` must be a live `&mut Response<false>` for the duration.
/// - `resp_obj` must be a live JSObject produced by `serve_resolve_response_value`.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn serve_write_response_object(
raw_cx: *mut JSContext,
res_mut: &mut Response<false>,
resp_obj: *mut JSObject,
) {
let mut wrapped_cx_resp = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(raw_cx));
let cx_ref_resp = &mut wrapped_cx_resp;
rooted!(&in(cx_ref_resp) let obj = resp_obj);
// status (default 200 if missing/invalid)
let mut status_val = UndefinedValue();
let _ = bao_stealth::engine_props::get_property_clearing(raw_cx, obj.handle().into(), c"status", &mut status_val);
let status_code: i32 = if status_val.is_int32() {
status_val.to_int32()
} else if status_val.is_double() {
status_val.to_double() as i32
} else {
200
};
let clamped = status_code.clamp(100, 599);
// Map status code → reason phrase for the status line. uWS wants the full
// "CODE REASON" form (matching `write_status(b"200 OK")` elsewhere).
let status_line = status_line_for(clamped);
res_mut.write_status(status_line.as_bytes());
// headers — a fetch-handler Response carries a WHATWG `Headers` instance
// (entries live in an internal Map, invisible to own-key enumeration), so
// first try the Headers face (`forEach` present → collect pairs by calling
// it with `arr.push` — forEach invokes callback(value, name) and push
// appends both, yielding a flat [v0, n0, v1, n1, …] array). Plain-object
// header maps (legacy/diy handler returns) keep the own-key enumeration.
let mut headers_val = UndefinedValue();
let _ = bao_stealth::engine_props::get_property_clearing(raw_cx, obj.handle().into(), c"headers", &mut headers_val);
if headers_val.is_object() {
rooted!(&in(cx_ref_resp) let headers_obj = headers_val.to_object());
let mut for_each_val = UndefinedValue();
let has_for_each = JS_GetProperty(
raw_cx,
headers_obj.handle().into(),
c"forEach".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut for_each_val,
},
);
let headers_face = has_for_each
&& for_each_val.is_object()
&& JS_ObjectIsFunction(for_each_val.to_object());
let mut pairs: Vec<(String, String)> = Vec::new();
if headers_face {
rooted!(&in(cx_ref_resp) let for_each_fn = for_each_val.to_object());
// Drive the JS `entries()` iterator (Headers.prototype.entries
// yields pre-joined [name, value] pairs). forEach(push, arr)
// would also append the 3rd callback arg (the Headers object
// itself) — push has no fixed arity — so the iterator protocol
// is the precise collector.
let mut entries_val = UndefinedValue();
let entries_ok = JS_GetProperty(
raw_cx,
headers_obj.handle().into(),
c"entries".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut entries_val,
},
);
if entries_ok && entries_val.is_object() {
rooted!(&in(cx_ref_resp) let entries_fn = entries_val.to_object());
rooted!(&in(cx_ref_resp) let iter_val = {
let mut r = UndefinedValue();
let r_h = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut r,
};
let empty_args = HandleValueArray {
length_: 0,
elements_: ::std::ptr::null(),
};
rooted!(&in(cx_ref_resp) let entries_cb = ObjectValue(entries_fn.get()));
if !JS_CallFunctionValue(
raw_cx,
headers_obj.handle().into(),
entries_cb.handle().into(),
&empty_args,
r_h,
) {
// A throwing Headers subclass must not poison the
// serializer's subsequent reads.
JS_ClearPendingException(raw_cx);
}
r
});
if iter_val.is_object() {
rooted!(&in(cx_ref_resp) let iter_obj = iter_val.to_object());
// Bounded walk (defensive: a broken iterator must not
// spin the serializer forever).
for _ in 0..256 {
rooted!(&in(cx_ref_resp) let next_val = {
let mut r = UndefinedValue();
let r_h = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut r,
};
let empty_args = HandleValueArray {
length_: 0,
elements_: ::std::ptr::null(),
};
let mut next_fn = UndefinedValue();
let got_next = JS_GetProperty(
raw_cx,
iter_obj.handle().into(),
c"next".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut next_fn,
},
);
if !got_next || !next_fn.is_object() {
r = UndefinedValue();
} else {
rooted!(&in(cx_ref_resp) let next_cb = ObjectValue(next_fn.to_object()));
if !JS_CallFunctionValue(
raw_cx,
iter_obj.handle().into(),
next_cb.handle().into(),
&empty_args,
r_h,
) {
JS_ClearPendingException(raw_cx);
}
}
r
});
if !next_val.is_object() {
break;
}
rooted!(&in(cx_ref_resp) let next_obj = next_val.to_object());
let mut done_val = UndefinedValue();
let got_done = JS_GetProperty(
raw_cx,
next_obj.handle().into(),
c"done".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut done_val,
},
);
if !got_done {
JS_ClearPendingException(raw_cx);
break;
}
if done_val.is_boolean() && done_val.to_boolean() {
break;
}
let mut pair_val = UndefinedValue();
let got_pair = JS_GetProperty(
raw_cx,
next_obj.handle().into(),
c"value".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut pair_val,
},
);
if !got_pair || !pair_val.is_object() {
JS_ClearPendingException(raw_cx);
break;
}
rooted!(&in(cx_ref_resp) let pair_obj = pair_val.to_object());
let mut name_v = UndefinedValue();
let mut value_v = UndefinedValue();
let got_name = JS_GetElement(
raw_cx,
pair_obj.handle().into(),
0,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut name_v,
},
);
let got_value = JS_GetElement(
raw_cx,
pair_obj.handle().into(),
1,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut value_v,
},
);
if got_name && got_value && name_v.is_string() && value_v.is_string() {
pairs.push((
crate::js_to_rust_string(raw_cx, name_v),
crate::js_to_rust_string(raw_cx, value_v),
));
}
}
}
}
}
if headers_face && !pairs.is_empty() {
for (key, val_str) in pairs {
if key.eq_ignore_ascii_case("content-length") {
continue;
}
let c_key = ZBox::from_bytes(key.as_bytes());
let c_v = ZBox::from_bytes(val_str.as_bytes());
res_mut.write_header(c_key.as_bytes(), c_v.as_bytes());
}
} else {
// Plain-object headers: property enumeration via `GetPropertyKeys`
// (mozjs Rust wrapper) + `IdVector` — the canonical pattern used in
// node_url.rs / node_util.rs for iterating JS object keys without
// raw AutoIdArray struct layout assumptions.
let mut ids = mozjs::rust::IdVector::new(cx_ref_resp);
let ok = GetPropertyKeys(
raw_cx,
headers_obj.handle().into(),
JSITER_OWNONLY,
ids.handle_mut(),
);
if ok {
for jsid in &*ids {
if !jsid.is_string() {
continue;
}
let key_str_ptr = jsid.to_string();
let key = jsstr_to_string(&mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(raw_cx)), NonNull::new_unchecked(key_str_ptr));
let mut header_val = UndefinedValue();
let c_key = ZBox::from_bytes(key.as_bytes());
JS_GetProperty(
raw_cx,
headers_obj.handle().into(),
c_key.as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut header_val,
},
);
let val_str = if header_val.is_string() {
crate::js_to_rust_string(raw_cx, header_val)
} else {
continue;
};
// Skip Content-Length — uWS recomputes it from the body
// bytes passed to `end`.
if key.eq_ignore_ascii_case("content-length") {
continue;
}
let c_v = ZBox::from_bytes(val_str.as_bytes());
res_mut.write_header(c_key.as_bytes(), c_v.as_bytes());
}
}
}
}
// body — fetch-handler Responses carry one of the body slots the
// Request/Response constructors write: `_bodyText` (string) or
// `_bodyBytes` (Uint8Array), read in that precedence (mirrors
// fetch_fn's Request-side read). Byte-exact: `_bodyBytes` goes to the
// wire verbatim via collect_byte_view — reading only `_bodyText` used
// to silently empty-body every ArrayBuffer/typed-array/Buffer response
// (`new Response(new Uint8Array(...))` stored its body in `_bodyBytes`).
let mut body_val = UndefinedValue();
let _ = bao_stealth::engine_props::get_property_clearing(raw_cx, obj.handle().into(), c"_bodyText", &mut body_val);
let body_bytes: Vec<u8> = if body_val.is_string() {
crate::js_to_rust_string(raw_cx, body_val).into_bytes()
} else {
let mut bytes_val = UndefinedValue();
let _ = bao_stealth::engine_props::get_property_clearing(raw_cx, obj.handle().into(), c"_bodyBytes", &mut bytes_val);
if bytes_val.is_object() {
crate::node_buffer::collect_byte_view(raw_cx, bytes_val).unwrap_or_default()
} else {
Vec::new()
}
};
// Content-Length is left to uWS `end()` (it derives framing from the
// final body write). Writing it here produced a duplicated header.
res_mut.end(&body_bytes, true);
}
/// Write the default `{"method":"...","url":"..."}` response used when no
/// fetch handler is registered or the handler cannot be resolved. Kept for
/// backward compatibility with `Bun.serve({ port: 0 })` callers that rely on
/// the diagnostic response shape.
///
/// @trace REQ-ENG-006 [api:Bun.serve default response]
/// The `method` is upper-cased before serialization: uWS hands us the
/// lowercase method token (per HTTP/1.1 case-insensitive convention), but
/// consumers (e.g. `tests/test_http_depth.js`) and Bun's diagnostic echo
/// expect the canonical uppercase form ("GET"/"POST"/...). Mirrors the
/// behavior the legacy synchronous path produced.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn serve_write_default_response(
res_mut: &mut Response<false>,
method: &[u8],
url: &[u8],
) {
// Works off the captured phase-1 copies — the uWS Request is long dead
// by the time this runs (phase 2).
let method_str_lower = ::std::str::from_utf8(method).unwrap_or("get");
let method_str = method_str_lower.to_ascii_uppercase();
let url_str = ::std::str::from_utf8(url).unwrap_or("/");
let body = serde_json::json!({
"method": method_str,
"url": url_str,
})
.to_string();
let body_bytes = body.as_bytes();
res_mut.write_status(b"200 OK");
res_mut.write_header(b"Content-Type", b"application/json");
res_mut.end(body_bytes, true);
}
/// Map an HTTP status code to its full status line ("CODE REASON").
/// Covers the common codes; unknown codes fall back to just the number.
fn status_line_for(code: i32) -> String {
let reason = match code {
100 => "Continue",
101 => "Switching Protocols",
200 => "OK",
201 => "Created",
202 => "Accepted",
204 => "No Content",
206 => "Partial Content",
301 => "Moved Permanently",
302 => "Found",
304 => "Not Modified",
307 => "Temporary Redirect",
308 => "Permanent Redirect",
400 => "Bad Request",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
405 => "Method Not Allowed",
408 => "Request Timeout",
409 => "Conflict",
410 => "Gone",
413 => "Payload Too Large",
414 => "URI Too Long",
415 => "Unsupported Media Type",
426 => "Upgrade Required",
429 => "Too Many Requests",
500 => "Internal Server Error",
501 => "Not Implemented",
502 => "Bad Gateway",
503 => "Service Unavailable",
504 => "Gateway Timeout",
_ => "",
};
if reason.is_empty() {
format!("{}", code)
} else {
format!("{} {}", code, reason)
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_gc(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
// W10-impl (design /tmp/w10-zone-fix-design.md §4-B): `Bun.gc` semantics =
// "return memory NOW" — SM 153 expresses that as GCOptions::Shrink (not a
// GCReason): shouldDecommit() is unconditionally true, the sweep tail runs
// purgeSourceURLs/relazify/propmap-purge and compacting moves survivors
// back so empty chunks decommit. GCReason::API skipped all of that in
// high-frequency allocation modes (GC.cpp:2280).
NonIncrementalGC(cx, JS::GCOptions::Shrink, JS::GCReason::API);
args.rval().set(UndefinedValue());
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_sleep(_cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 {
args.rval().set(UndefinedValue());
return true;
}
let val = *args.get(0).ptr;
let ms = if val.is_int32() {
val.to_int32() as u64
} else if val.is_double() {
val.to_double() as u64
} else {
0
};
::std::thread::sleep(::std::time::Duration::from_millis(ms));
args.rval().set(UndefinedValue());
true
}
#[allow(unsafe_op_in_unsafe_fn)]
/// Windows (WINRED-D3): attach the drive a rooted or drive-qualified path
/// resolves against (node path.win32.resolve semantics — the current drive,
/// taken from `orig`'s own prefix when present, else the process cwd's).
/// `normalize_path` output is '/'-rooted with any drive prefix dropped, so
/// the re-attachment happens here, keeping the forward-slash shape.
#[cfg(windows)]
fn qualify_windows_drive(lex: ::std::path::PathBuf, orig: &::std::path::Path) -> ::std::path::PathBuf {
let orig_s = orig.to_string_lossy().into_owned();
let b = orig_s.as_bytes();
let drive_prefixed = b.len() >= 2 && b[0].is_ascii_alphabetic() && b[1] == b':';
let rooted = b.first() == Some(&b'/') || b.first() == Some(&b'\\');
if !drive_prefixed && !rooted {
// Relative origin — nothing to anchor to a drive.
return lex;
}
let drive = if drive_prefixed {
orig_s[..2].to_string()
} else {
match ::std::env::current_dir() {
Ok(cwd) => {
let c = cwd.to_string_lossy().into_owned();
let cb = c.as_bytes();
if cb.len() >= 2 && cb[0].is_ascii_alphabetic() && cb[1] == b':' {
c[..2].to_string()
} else {
// No drive-qualified cwd available — keep the rooted form.
return lex;
}
}
Err(_) => return lex,
}
};
let mut s = lex.to_string_lossy().into_owned();
while s.starts_with('/') || s.starts_with('\\') {
s.remove(0);
}
::std::path::PathBuf::from(format!("{}/{}", drive, s))
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_resolve(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 {
JS_ReportErrorUTF8(cx, c"Bun.resolve requires a specifier".as_ptr());
return false;
}
let spec_val = *args.get(0).ptr;
if !spec_val.is_string() {
JS_ReportErrorUTF8(cx, c"Bun.resolve requires a string".as_ptr());
return false;
}
let specifier =
mozjs::conversions::jsstr_to_string(&mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx)), NonNull::new_unchecked(spec_val.to_string()));
let from = if argc > 1 && (*args.get(1).ptr).is_string() {
let from_str = mozjs::conversions::jsstr_to_string(&mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx)),
NonNull::new_unchecked((*args.get(1).ptr).to_string()),
);
Some(::std::path::PathBuf::from(from_str))
} else {
::std::env::current_dir().ok()
};
let spec_path = ::std::path::Path::new(&specifier);
// Windows (WINRED-D3): node's win32 rule counts a rooted-but-drive-less
// path ('/etc') — and any drive-prefixed path ('C:foo', 'C:/etc') — as
// absolute; std's Path::is_absolute demands a drive prefix and answered
// false, so '/etc' fell into the node_modules arm and threw
// "Cannot resolve". Mirrors the path.isAbsolute platform fix (node_path).
let specifier_is_absolute = {
let b = specifier.as_bytes();
#[cfg(windows)]
{
let rooted = b.first() == Some(&b'/') || b.first() == Some(&b'\\');
let drive = b.len() >= 2 && b[0].is_ascii_alphabetic() && b[1] == b':';
rooted || drive
}
#[cfg(not(windows))]
{
b.first() == Some(&b'/')
}
};
#[cfg(windows)]
let dot_rel = |spec: &str| {
spec.starts_with("./")
|| spec.starts_with("../")
|| spec.starts_with(".\\")
|| spec.starts_with("..\\")
};
#[cfg(not(windows))]
let dot_rel = |spec: &str| spec.starts_with("./") || spec.starts_with("../");
let resolved = if specifier_is_absolute {
spec_path.to_path_buf()
} else if dot_rel(&specifier) {
let base = from.as_deref().unwrap_or(::std::path::Path::new("."));
base.join(&specifier)
} else {
match crate::require::resolve_node_modules(&specifier, from.as_deref()) {
Some(p) => {
let s = p.to_string_lossy().into_owned();
let js_str = js_string_from_utf8(cx, &s);
if !js_str.is_null() {
args.rval().set(mozjs::jsval::StringValue(&*js_str));
} else {
args.rval().set(UndefinedValue());
}
return true;
}
None => {
let msg = format!("Cannot resolve '{}'", specifier);
let c_msg = ZBox::from_bytes(msg.as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
return false;
}
}
};
// Lexical normalize BEFORE canonicalize: canonicalize fails outright on
// non-existent paths, and the raw join keeps "./"/".." segments
// ('/tmp/./x.js') — upstream Bun.resolve normalizes them away
// ('/tmp/x.js'). Reuses node_path's normalize_path (REQ-ENG-007).
let lexical = crate::node_path::normalize_path(&resolved);
// Windows (WINRED-D3): node resolves a rooted or drive-qualified path
// against the CURRENT DRIVE (path.win32.resolve('/etc') → 'C:\etc').
// normalize_path is drive-blind (std's Disk prefix has no representation
// in its '/'-rooted output), so re-attach the drive after normalization.
#[cfg(windows)]
let lexical = {
let q = qualify_windows_drive(lexical, &resolved);
// Bun.resolve is '/'-shaped on every platform (the glob/posix face
// normalization this tree already applies elsewhere): the std lex
// path carries MAIN_SEPARATOR on win32.
::std::path::PathBuf::from(q.to_string_lossy().replace('\\', "/"))
};
let canonical = lexical.canonicalize().unwrap_or(lexical);
let s = canonical.to_string_lossy().into_owned();
let js_str = js_string_from_utf8(cx, &s);
if !js_str.is_null() {
args.rval().set(mozjs::jsval::StringValue(&*js_str));
} else {
args.rval().set(UndefinedValue());
}
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_which(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 {
args.rval().set(NullValue());
return true;
}
let name_val = *args.get(0).ptr;
if !name_val.is_string() {
args.rval().set(NullValue());
return true;
}
let name = crate::js_to_rust_string(cx, name_val);
let path_var = ::std::env::var("PATH").unwrap_or_default();
let separator = if cfg!(windows) { ';' } else { ':' };
for dir in path_var.split(separator) {
let candidate = ::std::path::Path::new(dir).join(&name);
if candidate.exists() {
let result = candidate.to_string_lossy().into_owned();
let js_str = js_string_from_utf8(cx, &result);
if !js_str.is_null() {
args.rval().set(StringValue(&*js_str));
} else {
args.rval().set(NullValue());
}
return true;
}
#[cfg(target_family = "unix")]
{
let candidate = ::std::path::Path::new(dir).join(&name);
if candidate.exists() {
let result = candidate.to_string_lossy().into_owned();
let js_str = js_string_from_utf8(cx, &result);
if !js_str.is_null() {
args.rval().set(StringValue(&*js_str));
} else {
args.rval().set(NullValue());
}
return true;
}
}
}
args.rval().set(NullValue());
true
}
#[allow(unsafe_op_in_unsafe_fn)]
// @trace REQ-ENG-006 [api:Bun.build] — Bun.build(config) JS face.
//
// Parses the upstream `BuildConfig` object into the Rust-only
// `NativeBuildConfig` and delegates to `bun_build::start` (BuildTasklet):
// the native bundle runs on a dedicated thread (the pipeline fans out to the
// shared CountedTask pool) and the returned Promise settles on the JS thread
// with the upstream `BuildOutput` shape —
// `{ success: boolean, outputs: BuildArtifact[], logs: BuildMessage[] }`.
//
// Semantics (mirrors upstream JSBundler.zig `build()`):
// * Invalid config (non-object / missing or empty `entrypoints` /
// non-string entries) THROWS — `throwInvalidArguments` parity.
// * Build failures (unresolvable entry, syntax error, …) RESOLVE with
// `success:false` + `logs` — never reject (Bun.build never throws for
// build-level failures; `throw: false` is the only mode).
// * Unknown config keys are accepted and ignored (upstream tolerates
// extra keys); unsupported-by-port keys are surfaced in the result logs
// by the native driver, never silently dropped.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_build(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 || !(*args.get(0).ptr).is_object() {
JS_ReportErrorUTF8(
cx,
c"Expected a config object to be passed to Bun.build".as_ptr(),
);
return false;
}
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let cfg = (*args.get(0).ptr).to_object());
let cfg_h = cfg.handle().into();
// ── entrypoints (required, non-empty, string[]) ─────────────────────
let mut entrypoints: Vec<String> = Vec::new();
let mut has_ep: bool = false;
JS_HasProperty(cx, cfg_h, c"entrypoints".as_ptr(), &mut has_ep);
if has_ep {
rooted!(&in(cx_ref) let mut ep_val = UndefinedValue());
JS_GetProperty(cx, cfg_h, c"entrypoints".as_ptr(), ep_val.handle_mut().into());
if ep_val.get().is_object() {
rooted!(&in(cx_ref) let ep_obj = ep_val.get().to_object());
let mut len_val = UndefinedValue();
let len_rv = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut len_val,
};
JS_GetProperty(cx, ep_obj.handle().into(), c"length".as_ptr(), len_rv);
if len_val.is_number() {
let len = len_val.to_number() as u32;
for i in 0..len {
let mut item_val = UndefinedValue();
let item_rv = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut item_val,
};
JS_GetElement(cx, ep_obj.handle().into(), i, item_rv);
if item_val.is_string() {
entrypoints.push(jsstr_to_string(&mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx)),
NonNull::new_unchecked(item_val.to_string()),
));
}
}
}
}
}
if entrypoints.is_empty() {
JS_ReportErrorUTF8(
cx,
c"Bun.build: expected a non-empty array of entrypoints".as_ptr(),
);
return false;
}
let mut config = crate::bun_build::NativeBuildConfig {
entrypoints,
..Default::default()
};
// ── scalar / string options ──────────────────────────────────────────
config.outdir = get_opt_string(cx, cfg_h, c"outdir");
config.root = get_opt_string(cx, cfg_h, c"root");
config.public_path = get_opt_string(cx, cfg_h, c"publicPath");
config.banner = get_opt_string(cx, cfg_h, c"banner");
config.footer = get_opt_string(cx, cfg_h, c"footer");
// naming: string | { entry, chunk, asset } — the string form maps to
// the entry template (upstream BuildConfig.Naming).
config.naming = get_opt_string(cx, cfg_h, c"naming");
{
rooted!(&in(cx_ref) let mut naming_val = UndefinedValue());
JS_GetProperty(cx, cfg_h, c"naming".as_ptr(), naming_val.handle_mut().into());
if naming_val.get().is_object() {
rooted!(&in(cx_ref) let naming_obj = naming_val.get().to_object());
let naming_h = naming_obj.handle().into();
config.naming_entry = get_opt_string(cx, naming_h, c"entry");
config.naming_chunk = get_opt_string(cx, naming_h, c"chunk");
config.naming_asset = get_opt_string(cx, naming_h, c"asset");
config.naming = None;
}
}
// target: "browser" (default) | "bun" | "node"
config.target = get_opt_string(cx, cfg_h, c"target").unwrap_or_else(|| "browser".into());
// format: "esm" (default) | "cjs" | "iife"
config.format = get_opt_string(cx, cfg_h, c"format").unwrap_or_else(|| "esm".into());
// sourcemap: "none" (default) | "linked" | "inline" | "external"
config.sourcemap = get_opt_string(cx, cfg_h, c"sourcemap").unwrap_or_else(|| "none".into());
// ── minify: boolean | { whitespace, syntax, identifiers } ────────────
let mut has_minify: bool = false;
JS_HasProperty(cx, cfg_h, c"minify".as_ptr(), &mut has_minify);
if has_minify {
rooted!(&in(cx_ref) let mut min_val = UndefinedValue());
JS_GetProperty(cx, cfg_h, c"minify".as_ptr(), min_val.handle_mut().into());
if min_val.get().is_boolean() {
if min_val.get().to_boolean() {
config.minify = crate::bun_build::NativeMinify::all();
}
} else if min_val.get().is_object() {
rooted!(&in(cx_ref) let min_obj = min_val.get().to_object());
let min_h = min_obj.handle().into();
config.minify = crate::bun_build::NativeMinify {
whitespace: get_opt_bool(cx, min_h, c"whitespace").unwrap_or(false),
syntax: get_opt_bool(cx, min_h, c"syntax").unwrap_or(false),
identifiers: get_opt_bool(cx, min_h, c"identifiers").unwrap_or(false),
};
}
}
// ── external: string[] ────────────────────────────────────────────────
let mut has_ext: bool = false;
JS_HasProperty(cx, cfg_h, c"external".as_ptr(), &mut has_ext);
if has_ext {
rooted!(&in(cx_ref) let mut ext_val = UndefinedValue());
JS_GetProperty(cx, cfg_h, c"external".as_ptr(), ext_val.handle_mut().into());
if ext_val.get().is_object() {
rooted!(&in(cx_ref) let ext_obj = ext_val.get().to_object());
let mut len_val = UndefinedValue();
let len_rv = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut len_val,
};
JS_GetProperty(cx, ext_obj.handle().into(), c"length".as_ptr(), len_rv);
if len_val.is_number() {
let len = len_val.to_number() as u32;
for i in 0..len {
let mut item_val = UndefinedValue();
let item_rv = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut item_val,
};
JS_GetElement(cx, ext_obj.handle().into(), i, item_rv);
if item_val.is_string() {
config.external.push(jsstr_to_string(&mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx)),
NonNull::new_unchecked(item_val.to_string()),
));
}
}
}
}
}
// ── define: Record<string, string> ───────────────────────────────────
let mut has_def: bool = false;
JS_HasProperty(cx, cfg_h, c"define".as_ptr(), &mut has_def);
if has_def {
rooted!(&in(cx_ref) let mut def_val = UndefinedValue());
JS_GetProperty(cx, cfg_h, c"define".as_ptr(), def_val.handle_mut().into());
if def_val.get().is_object() {
rooted!(&in(cx_ref) let def_obj = def_val.get().to_object());
// Own enumerable string keys (Headers record parsing pattern).
let mut ids = mozjs::rust::IdVector::new(cx_ref);
if GetPropertyKeys(cx, def_obj.handle().into(), JSITER_OWNONLY, ids.handle_mut()) {
for jsid in &*ids {
if !jsid.is_string() {
continue;
}
let key_ptr = jsid.to_string();
if key_ptr.is_null() {
continue;
}
let key = jsstr_to_string(&mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx)), NonNull::new_unchecked(key_ptr));
let c_key = ZBox::from_bytes(key.as_bytes());
rooted!(&in(cx_ref) let mut v_val = UndefinedValue());
JS_GetProperty(
cx,
def_obj.handle().into(),
c_key.as_ptr(),
v_val.handle_mut().into(),
);
let value = if v_val.get().is_string() {
jsstr_to_string(&mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx)),
NonNull::new_unchecked(v_val.get().to_string()),
)
} else if v_val.get().is_number() {
format!("{}", v_val.get().to_number())
} else if v_val.get().is_boolean() {
if v_val.get().to_boolean() { "true" } else { "false" }.to_string()
} else {
continue;
};
config.define.push((key, value));
}
}
}
}
// ── splitting: boolean ───────────────────────────────────────────────
config.splitting = get_opt_bool(cx, cfg_h, c"splitting").unwrap_or(false);
// ── jsx: { runtime, factory, fragment, importSource, development } ──
let mut has_jsx: bool = false;
JS_HasProperty(cx, cfg_h, c"jsx".as_ptr(), &mut has_jsx);
if has_jsx {
rooted!(&in(cx_ref) let mut jsx_val = UndefinedValue());
JS_GetProperty(cx, cfg_h, c"jsx".as_ptr(), jsx_val.handle_mut().into());
if jsx_val.get().is_object() {
rooted!(&in(cx_ref) let jsx_obj = jsx_val.get().to_object());
let jsx_h = jsx_obj.handle().into();
config.jsx_runtime = get_opt_string(cx, jsx_h, c"runtime");
config.jsx_factory = get_opt_string(cx, jsx_h, c"factory");
config.jsx_fragment = get_opt_string(cx, jsx_h, c"fragment");
config.jsx_import_source = get_opt_string(cx, jsx_h, c"importSource");
config.jsx_development = get_opt_bool(cx, jsx_h, c"development");
}
}
// Pending Promise — settled on the JS thread by the BuildTasklet
// (resolve with BuildOutput, including the success:false error face).
rooted!(&in(cx_ref) let promise = JS::NewPromiseObject(cx, HandleObject::null()));
if promise.get().is_null() {
args.rval().set(UndefinedValue());
return false;
}
let promise_val = ObjectValue(promise.get());
crate::bun_build::start(cx, promise_val, config);
args.rval().set(promise_val);
true
}
/// Read an optional string property off a config object.
unsafe fn get_opt_string(
cx: *mut JSContext,
obj: mozjs::jsapi::Handle<*mut JSObject>,
name: &::std::ffi::CStr,
) -> Option<String> {
let mut has: bool = false;
// SAFETY: live JSContext + object handle; static name.
unsafe { JS_HasProperty(cx, obj, name.as_ptr(), &mut has) };
if !has {
return None;
}
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let mut val = UndefinedValue());
// SAFETY: property get on a live object.
unsafe { JS_GetProperty(cx, obj, name.as_ptr(), val.handle_mut().into()) };
if val.get().is_string() {
Some(jsstr_to_string(&mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx)),
NonNull::new_unchecked(val.get().to_string()),
))
} else {
None
}
}
/// Read an optional boolean property off a config object.
unsafe fn get_opt_bool(
cx: *mut JSContext,
obj: mozjs::jsapi::Handle<*mut JSObject>,
name: &::std::ffi::CStr,
) -> Option<bool> {
let mut has: bool = false;
// SAFETY: live JSContext + object handle; static name.
unsafe { JS_HasProperty(cx, obj, name.as_ptr(), &mut has) };
if !has {
return None;
}
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let mut val = UndefinedValue());
// SAFETY: property get on a live object.
unsafe { JS_GetProperty(cx, obj, name.as_ptr(), val.handle_mut().into()) };
if val.get().is_boolean() {
Some(val.get().to_boolean())
} else {
None
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_test(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc < 2 {
args.rval().set(UndefinedValue());
return true;
}
let name_val = *args.get(0).ptr;
let fn_val = *args.get(1).ptr;
if !name_val.is_string() || !fn_val.is_object() {
args.rval().set(UndefinedValue());
return true;
}
let name = jsstr_to_string(&mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx)), NonNull::new_unchecked(name_val.to_string()));
let mut wrapped_cx_test = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref_test = &mut wrapped_cx_test;
rooted!(&in(cx_ref_test) let callback = fn_val.to_object());
let cb_id = TEST_CB_COUNTER.fetch_add(1, Ordering::Relaxed);
let callback_key = format!("test_cb_{}", cb_id);
gc_store_insert(cx, &callback_key, callback.get());
TEST_REGISTRY.with(|reg| {
reg.borrow_mut().push(TestCase { name, callback_key });
});
args.rval().set(UndefinedValue());
true
}
unsafe extern "C" fn test_run(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
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 = CurrentGlobalOrNull(cx));
if global.get().is_null() {
args.rval().set(UndefinedValue());
return true;
}
let mut passed: u32 = 0;
let mut failed: u32 = 0;
let mut failures: Vec<String> = Vec::new();
let tests: Vec<TestCase> = TEST_REGISTRY.with(|reg| ::std::mem::take(&mut *reg.borrow_mut()));
for tc in &tests {
rooted!(&in(cx_ref) let cb_obj = match gc_store_get(cx, &tc.callback_key) {
Some(obj) => obj,
None => {
eprint!("\n\u{2717} {} (callback GC'd)\n", tc.name);
failures.push(tc.name.clone());
failed += 1;
continue;
}
});
rooted!(&in(cx_ref) let cb_h = ObjectValue(cb_obj.get()));
let empty_args = HandleValueArray::empty();
let mut rval = UndefinedValue();
let rval_h = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut rval,
};
let ok = JS_CallFunctionValue(
cx,
global.handle().into(),
cb_h.handle().into(),
&empty_args,
rval_h,
);
if ok {
eprint!("\n\u{2713} {}\n", tc.name);
passed += 1;
} else {
JS_ClearPendingException(cx);
eprint!("\n\u{2717} {}\n", tc.name);
failures.push(tc.name.clone());
failed += 1;
}
}
// Clean up GcStore entries for all test callbacks
for tc in &tests {
gc_store_remove(cx, &tc.callback_key);
}
let total = passed + failed;
eprint!(
"\n{} test(s) ran, {} passed, {} failed\n",
total, passed, failed
);
rooted!(&in(cx_ref) let result_obj = JS_NewPlainObject(cx_ref));
if result_obj.get().is_null() {
args.rval().set(UndefinedValue());
return true;
}
let obj_h = result_obj.handle().into();
rooted!(&in(cx_ref) let total_root = Int32Value(total as i32));
JS_DefineProperty(
cx,
obj_h,
c"total".as_ptr(),
total_root.handle().into(),
JSPROP_ENUMERATE as u32,
);
rooted!(&in(cx_ref) let passed_root = Int32Value(passed as i32));
JS_DefineProperty(
cx,
obj_h,
c"passed".as_ptr(),
passed_root.handle().into(),
JSPROP_ENUMERATE as u32,
);
rooted!(&in(cx_ref) let failed_root = Int32Value(failed as i32));
JS_DefineProperty(
cx,
obj_h,
c"failed".as_ptr(),
failed_root.handle().into(),
JSPROP_ENUMERATE as u32,
);
let success = failed == 0;
rooted!(&in(cx_ref) let success_root = BooleanValue(success));
JS_DefineProperty(
cx,
obj_h,
c"success".as_ptr(),
success_root.handle().into(),
JSPROP_ENUMERATE as u32,
);
if !failures.is_empty() {
rooted!(&in(cx_ref) let fail_arr = NewArrayObject1(cx_ref, 0));
for (i, fname) in failures.iter().enumerate() {
let js_str = js_string_from_utf8(cx, fname);
if !js_str.is_null() {
let fval = StringValue(&*js_str);
rooted!(&in(cx_ref) let fv2 = fval);
JS_SetElement(cx, fail_arr.handle().into(), i as u32, fv2.handle().into());
}
}
let fav = ObjectValue(fail_arr.get());
rooted!(&in(cx_ref) let favh = fav);
JS_DefineProperty(
cx,
obj_h,
c"failures".as_ptr(),
favh.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
args.rval().set(ObjectValue(result_obj.get()));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
/// Real parent pid on Windows via a Toolhelp32 process snapshot
/// (PROCESSENTRY32W, TH32CS_SNAPPROCESS). 0 only when snapshotting fails —
/// the honest "unavailable", not a silent constant (node reports the actual
/// parent through the same snapshot).
#[cfg(windows)]
fn windows_parent_pid() -> i32 {
#[repr(C)]
struct ProcessEntry32W {
dw_size: u32,
cnt_usage: u32,
th32_process_id: u32,
th32_default_heap_id: usize,
th32_module_id: u32,
cnt_threads: u32,
th32_parent_process_id: u32,
pc_pri_class_base: i32,
dw_flags: u32,
sz_exe_file: [u16; 260],
}
unsafe extern "system" {
fn CreateToolhelp32Snapshot(flags: u32, pid: u32) -> *mut core::ffi::c_void;
fn Process32FirstW(snapshot: *mut core::ffi::c_void, entry: *mut ProcessEntry32W) -> i32;
fn Process32NextW(snapshot: *mut core::ffi::c_void, entry: *mut ProcessEntry32W) -> i32;
fn CloseHandle(h: *mut core::ffi::c_void) -> i32;
}
const TH32CS_SNAPPROCESS: u32 = 0x2;
let me = ::std::process::id();
unsafe {
let snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if snap.is_null() || snap as usize == ::std::usize::MAX {
return 0;
}
let mut e = ProcessEntry32W {
dw_size: ::std::mem::size_of::<ProcessEntry32W>() as u32,
cnt_usage: 0,
th32_process_id: 0,
th32_default_heap_id: 0,
th32_module_id: 0,
cnt_threads: 0,
th32_parent_process_id: 0,
pc_pri_class_base: 0,
dw_flags: 0,
sz_exe_file: [0; 260],
};
let mut ppid = 0i32;
if Process32FirstW(snap, &mut e) != 0 {
loop {
if e.th32_process_id == me {
ppid = e.th32_parent_process_id as i32;
break;
}
if Process32NextW(snap, &mut e) == 0 {
break;
}
}
}
CloseHandle(snap);
ppid
}
}
unsafe extern "C" fn bun_file(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 || args.get(0).ptr.is_null() {
args.rval().set(UndefinedValue());
return true;
}
let path_val = *args.get(0).ptr;
let mut wrapped_cx_early = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_early = &mut wrapped_cx_early;
// Integer fd form: `Bun.file(fd)` (upstream BunFile supports raw file
// descriptors; Bun.stdin/stdout/stderr are Bun.file(0/1/2)). The object
// carries fd/path/readable/writable plus the same exists() face as the
// path form (added inside make_bun_file_for_fd).
if path_val.is_int32() || path_val.is_double() {
let fd = if path_val.is_int32() {
path_val.to_int32()
} else {
path_val.to_double() as i32
};
let fd_obj = make_bun_file_for_fd(cx_early, fd);
if fd_obj.is_null() {
args.rval().set(UndefinedValue());
return true;
}
args.rval().set(mozjs::jsval::ObjectValue(fd_obj));
return true;
}
// URL-object form: `Bun.file(new URL("file:///path"))`. file: URLs map to
// their decoded pathname (same conversion as Bun.fileURLToPath); other
// schemes are an explicit error — the lazy remote-BunFile face of
// upstream is not implemented here and must not silently fake a path.
// Detection is structural (`href` string property), so URL instances from
// any realm/constructor are accepted without instanceof coupling.
let mut s: String;
if !path_val.is_string() && path_val.is_object() {
rooted!(&in(cx_early) let url_obj = path_val.to_object());
let mut href_val = UndefinedValue();
let href_ok = JS_GetProperty(
cx,
url_obj.handle().into(),
c"href".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut href_val,
},
);
if !href_ok || !href_val.is_string() {
args.rval().set(UndefinedValue());
return true;
}
let href = crate::js_to_rust_string(cx, href_val);
let mut protocol_val = UndefinedValue();
let proto_ok = JS_GetProperty(
cx,
url_obj.handle().into(),
c"protocol".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut protocol_val,
},
);
let protocol = if proto_ok && protocol_val.is_string() {
crate::js_to_rust_string(cx, protocol_val)
} else {
String::new()
};
if protocol == "file:" {
let bun_str = bun_core::String::borrow_utf8(href.as_bytes());
let result = bun_url::whatwg::path_from_file_url(&bun_str);
let path = if result.tag() == bun_core::Tag::Dead {
href.clone()
} else {
let utf8 = result.to_utf8();
let p = ::std::str::from_utf8(utf8.slice()).unwrap_or(&href).to_string();
// bc713f9543: `result` owns its ref — `Drop` at scope exit is
// the old manual `.deref()` (after `utf8`'s own ref releases).
p
};
s = percent_decode_path(&path);
} else {
let msg = format!(
"Bun.file() does not support {} URLs in this runtime (only file: URLs)",
if protocol.is_empty() { "this" } else { protocol.as_str() }
);
let c_msg = ZBox::from_bytes(msg.as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
return false;
}
} else if path_val.is_string() {
s = crate::js_to_rust_string(cx, path_val);
} else {
args.rval().set(UndefinedValue());
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 file_obj = JS_NewPlainObject(cx_ref));
if file_obj.get().is_null() {
args.rval().set(UndefinedValue());
return true;
}
// The path property must carry the path's code points, not its UTF-8
// bytes relabeled Latin-1: JS_NewStringCopyZ turned "uni-世界.txt" into
// "uni-世界.txt", so exists()/text() re-stat'd a path that never
// existed. js_string_from_utf8 keeps the JS↔Rust path round-trip
// byte-exact (same encoder as the file text() reader).
let path_js_str = js_string_from_utf8(cx, &s);
if !path_js_str.is_null() {
rooted!(&in(cx_ref) let val = StringValue(&*path_js_str));
JS_DefineProperty(
cx,
file_obj.handle().into(),
c"path".as_ptr(),
val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
// size — snapshot when the target exists (stat at file() time).
if let Ok(meta) = bun_sys::fs::metadata(&s) {
rooted!(&in(cx_ref) let size_root = DoubleValue(meta.size as f64));
JS_DefineProperty(
cx,
file_obj.handle().into(),
c"size".as_ptr(),
size_root.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
// exists()/text()/json()/arrayBuffer()/slice() — the shared BunFile face
// (define_bunfile_methods; audit history: exists() resolves false for
// missing paths instead of throwing).
define_bunfile_methods(cx_ref, file_obj.handle());
args.rval().set(mozjs::jsval::ObjectValue(file_obj.get()));
true
}
/// `BunFile.exists()` — resolves to a live-stat boolean. Always defined on
/// every BunFile instance (missing file → `Promise.resolve(false)`, never a
/// throw).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_file_exists(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let this = args.thisv();
if !this.is_object() {
args.rval().set(UndefinedValue());
return true;
}
let mut wrapped = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped;
rooted!(&in(cx_ref) let obj = this.get().to_object());
let mut path_v = UndefinedValue();
let mut path = String::new();
if JS_GetProperty(
cx,
obj.handle().into(),
c"path".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut path_v,
},
) && path_v.is_string()
{
path = crate::js_to_rust_string(cx, path_v);
}
// Path-form stat first; fd-form fallback (WINRED 2026-09-26): the fd
// form's synthetic path (`/dev/stdin` & friends, `/proc/self/fd/N`) is
// a POSIX device/link path that never stats on Windows — and even on
// POSIX a deleted-but-open fd should report exists via its descriptor.
// When the object carries a live `fd`, the descriptor is the truth.
let path_exists = !path.is_empty() && bun_sys::fs::metadata(&path).is_ok();
let exists = if path_exists {
true
} else {
let mut fd_v = UndefinedValue();
let has_fd = JS_GetProperty(
cx,
obj.handle().into(),
c"fd".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut fd_v,
},
);
if has_fd && fd_v.is_int32() {
let fd = fd_v.to_int32();
if fd >= 0 {
#[cfg(unix)]
{
let mut st: libc::stat = unsafe { ::std::mem::zeroed() };
// SAFETY: fd is a number property on this BunFile; fstat
// only reads it.
unsafe { libc::fstat(fd, &mut st) == 0 }
}
#[cfg(windows)]
{
// CRT-level fstat on the fd (works for std streams and
// regular files; character devices included).
#[link(name = "ucrt")]
unsafe extern "C" {
fn _fstat64i32(fd: i32, out: *mut u8) -> i32;
}
let mut buf = [0u8; 160]; // struct _stat64i32
// SAFETY: fd from the JS property; out buffer sized for
// the CRT struct; call only reads the descriptor.
unsafe { _fstat64i32(fd, buf.as_mut_ptr()) == 0 }
}
} else {
false
}
} else {
false
}
};
rooted!(&in(cx_ref) let promise = JS::NewPromiseObject(cx, HandleObject::null()));
if promise.get().is_null() {
args.rval().set(mozjs::jsval::BooleanValue(exists));
return true;
}
rooted!(&in(cx_ref) let bv = mozjs::jsval::BooleanValue(exists));
let ok = JS::ResolvePromise(
cx,
promise.handle().into(),
bv.handle().into(),
);
args.rval().set(if ok {
mozjs::jsval::ObjectValue(promise.get())
} else {
JS_ClearPendingException(cx);
mozjs::jsval::BooleanValue(exists)
});
true
}
// ──────────────────────────────────────────────────────────────────────────
// @trace REQ-ENG-006 [api:Bun.file BunFile.text/json/arrayBuffer/slice] —
// the BunFile read method family, shared by the path form (bun_file) and the
// fd form (make_bun_file_for_fd).
// ──────────────────────────────────────────────────────────────────────────
/// Attach the full BunFile method face (exists + the read family) to a
/// BunFile instance. Both construction forms expose the identical surface.
unsafe fn define_bunfile_methods(
cx: &mut mozjs::context::JSContext,
obj: mozjs::rust::Handle<*mut JSObject>,
) {
// exists() — Promise<boolean> over a live stat (upstream BunFile shape).
JS_DefineFunction(
cx,
obj,
c"exists".as_ptr(),
Some(bun_file_exists),
0,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
obj,
c"text".as_ptr(),
Some(bun_file_text),
0,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
obj,
c"json".as_ptr(),
Some(bun_file_json),
0,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
obj,
c"arrayBuffer".as_ptr(),
Some(bun_file_array_buffer),
0,
JSPROP_ENUMERATE as u32,
);
// slice(begin, end, contentType) — sync Blob return (upstream BunFile
// extends Blob; slice never promises).
JS_DefineFunction(
cx,
obj,
c"slice".as_ptr(),
Some(bun_file_slice),
3,
JSPROP_ENUMERATE as u32,
);
// writer() — FileSink face (upstream BunFile.writer()); writes go
// incrementally to the real descriptor.
// @trace REQ-ENG-007 [api:Bun.file.writer FileSink]
JS_DefineFunction(
cx,
obj,
c"writer".as_ptr(),
Some(bun_file_writer),
0,
JSPROP_ENUMERATE as u32,
);
}
// ──────────────────────────────────────────────────────────────────────────
// @trace REQ-ENG-007 [api:Bun.file.writer FileSink] — BunFile.writer()
//
// `Bun.file(path).writer()` / `Bun.file(fd).writer()` return a FileSink: a
// buffered, REAL-descriptor streaming writer (port of the upstream
// webcore::FileSink face, scoped to this runtime's synchronous
// JS-thread write path).
//
// Semantics (mirroring the upstream FileSink):
// - write(chunk) → number: accepts string (UTF-8 bytes) / typed array /
// ArrayBuffer, appends to the sink buffer, and auto-flushes through to
// the fd once the buffer reaches `highWaterMark` (default 16 KiB).
// Returns the number of bytes accepted by this call. After end() the
// sink is dead and every write reports 0 — the bytes are never silently
// dropped into a live buffer.
// - flush() → Promise<number>: writes the buffered bytes to the fd,
// resolving with the byte count actually flushed.
// - end() → Promise<number>: flushes what remains, closes the descriptor
// (only when the sink opened it — a `Bun.file(fd)` writer never closes a
// descriptor it does not own), and resolves with the total bytes
// written. The sink state is torn down; the final total is preserved on
// the JS object so a second end() resolves with the same number instead
// of dereferencing freed state.
// - ref() / unref() → this: passthrough faces. In this design writes
// complete synchronously to the fd on the JS thread (no event-loop
// pending-write state exists to keep alive), so the faces are
// identity-preserving for API compatibility.
//
// Ownership / GC discipline: the Rust state lives in a heap Box whose
// pointer is stored in the sink object's `_sinkPtr` PrivateValue slot (the
// same full-64-bit pattern as the server object's `_appPtr`). No JSObject
// pointer crosses threads and no raw pointer survives past end(); writes
// happen inline on the JS thread. Delta vs upstream (which closes at GC
// finalize): a sink that is abandoned WITHOUT end() keeps its descriptor
// until process exit — end() is the documented close point, matching the
// observable contract of every FileSink repro.
// ──────────────────────────────────────────────────────────────────────────
/// Rust-side FileSink state (held by the sink object's `_sinkPtr` slot).
struct BunFileSinkState {
/// Owning handle — Some only when the sink opened the file itself (the
/// path form). Dropped (closing the descriptor) at end(). The stdio
/// descriptors are never closed by the Drop guard.
file: Option<bun_sys::File>,
/// Raw descriptor used for every write (owned or borrowed form). Kept in
/// the platform-faithful `Fd` form (c_int on POSIX, HANDLE-tagged u64 on
/// Windows) so the borrowed-form write path hands it straight to
/// `File::borrow` on both platforms.
fd: bun_sys::Fd,
/// Display identity for error messages.
display: String,
/// Buffered bytes not yet handed to the fd.
buf: Vec<u8>,
/// Auto-flush threshold (upstream highWaterMark default 16 KiB).
high_water_mark: usize,
/// Total bytes handed to the fd so far.
written_total: u64,
/// Set by end() — a dead sink accepts nothing further.
ended: bool,
}
impl BunFileSinkState {
/// Write everything currently buffered to the fd. Returns the number of
/// bytes flushed by THIS call.
fn flush_buf(&mut self) -> ::std::result::Result<u64, ::std::io::Error> {
if self.buf.is_empty() {
return Ok(0);
}
let n = self.buf.len() as u64;
// Write path: the owned handle when present, otherwise a non-owning
// view of the borrowed descriptor (`Bun.file(fd).writer()` never
// takes ownership of the caller's descriptor).
let result = match self.file.as_ref() {
Some(f) => f.write_all(&self.buf),
None => bun_sys::File::borrow(&self.fd).write_all(&self.buf),
};
match result {
Ok(()) => {
self.written_total += n;
self.buf.clear();
Ok(n)
}
Err(e) => Err(::std::io::Error::from_raw_os_error(e.errno as i32)),
}
}
}
/// Read the sink state pointer off a FileSink receiver.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn bun_file_sink_state(
cx: *mut JSContext,
this: Handle<Value>,
) -> Option<*mut BunFileSinkState> {
if !this.is_object() {
return None;
}
let mut wrapped = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped;
rooted!(&in(cx_ref) let obj = this.get().to_object());
let mut ptr_val = UndefinedValue();
JS_GetProperty(
cx,
obj.handle().into(),
c"_sinkPtr".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut ptr_val,
},
);
if !ptr_val.is_double() || (ptr_val.asBits_ & 0xFFFF000000000000) != 0 {
// Absent / a plain double — the sink already ended (the slot was
// cleared) or this is not a FileSink at all.
return None;
}
let ptr = ptr_val.to_private() as *mut BunFileSinkState;
if ptr.is_null() {
None
} else {
Some(ptr)
}
}
/// `BunFile.writer(options?)` — create the FileSink.
///
/// The path form opens the target with write+create+truncate (the upstream
/// `Bun.file(path).writer()` open mode); the fd form writes to the live
/// descriptor WITHOUT taking ownership of it.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_file_writer(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let this = args.thisv();
if !this.is_object() {
JS_ReportErrorUTF8(cx, c"BunFile.writer: not a BunFile receiver".as_ptr());
return false;
}
// Optional { highWaterMark } (upstream writer option).
let mut high_water_mark: usize = 16 * 1024;
if argc > 0 {
let opts_val = *args.get(0).ptr;
if opts_val.is_object() {
let mut wrapped = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped;
rooted!(&in(cx_ref) let opts_obj = opts_val.to_object());
let mut hwm_val = UndefinedValue();
JS_GetProperty(
cx,
opts_obj.handle().into(),
c"highWaterMark".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut hwm_val,
},
);
if hwm_val.is_int32() {
let n = hwm_val.to_int32();
if n > 0 {
high_water_mark = n as usize;
}
} else if hwm_val.is_double() {
let n = hwm_val.to_double();
if n > 0.0 {
high_water_mark = n as usize;
}
}
}
}
let src = match bunfile_src(cx, this) {
Some(s) => s,
None => {
JS_ReportErrorUTF8(
cx,
c"BunFile.writer: receiver has no readable path or fd".as_ptr(),
);
return false;
}
};
let (file, fd, display) = match src {
BunfileSrc::Path(path) => {
// Same open discipline as Bun.write (unix libc flags; Windows
// decodes through bun_sys::O). Write + create + truncate: the
// writer owns the target from offset 0.
let zpath = ZBox::from_bytes(path.as_bytes());
match bun_sys::File::open(
zpath.as_zstr(),
bun_sys::O::WRONLY | bun_sys::O::CREAT | bun_sys::O::TRUNC | bun_sys::O::CLOEXEC,
0o666 as bun_core::Mode,
) {
Ok(f) => {
let fd = f.fd();
(Some(f), fd, path)
}
Err(e) => {
let code = bunfile_io_code(&::std::io::Error::from_raw_os_error(e.errno as i32));
let msg = format!("BunFile.writer: failed to open {}: errno {}", path, e.errno);
let c_code = ZBox::from_bytes(code.as_bytes());
let c_msg = ZBox::from_bytes(msg.as_bytes());
JS_ReportErrorUTF8(cx, c"%s: %s".as_ptr(), c_code.as_ptr(), c_msg.as_ptr());
return false;
}
}
}
BunfileSrc::Fd(fd) => {
// Borrowed descriptor — never closed by this sink.
(None, fd, format!("/proc/self/fd/{}", fd.uv()))
}
};
let state = Box::new(BunFileSinkState {
file,
fd,
display,
buf: Vec::new(),
high_water_mark,
written_total: 0,
ended: false,
});
let state_ptr = Box::into_raw(state);
let mut wrapped = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped;
rooted!(&in(cx_ref) let sink_obj = JS_NewPlainObject(cx_ref));
if sink_obj.get().is_null() {
drop(Box::from_raw(state_ptr));
JS_ReportErrorUTF8(cx, c"BunFile.writer: failed to create the sink object".as_ptr());
return false;
}
let sink_h = sink_obj.handle().into();
// `_sinkPtr` — PrivateValue preserves the full 64-bit pointer (same
// pattern as the server object's `_appPtr`).
let ptr_val = mozjs::jsval::PrivateValue(state_ptr as *const core::ffi::c_void);
rooted!(&in(cx_ref) let ptr_root = ptr_val);
JS_DefineProperty(cx, sink_h, c"_sinkPtr".as_ptr(), ptr_root.handle().into(), 0);
// Chainable passthrough faces + the write family.
JS_DefineFunction(cx_ref, sink_obj.handle(), c"write".as_ptr(), Some(bun_file_sink_write), 1, JSPROP_ENUMERATE as u32);
JS_DefineFunction(cx_ref, sink_obj.handle(), c"flush".as_ptr(), Some(bun_file_sink_flush), 0, JSPROP_ENUMERATE as u32);
JS_DefineFunction(cx_ref, sink_obj.handle(), c"end".as_ptr(), Some(bun_file_sink_end), 0, JSPROP_ENUMERATE as u32);
JS_DefineFunction(cx_ref, sink_obj.handle(), c"ref".as_ptr(), Some(bun_file_sink_ref), 0, 0);
JS_DefineFunction(cx_ref, sink_obj.handle(), c"unref".as_ptr(), Some(bun_file_sink_unref), 0, 0);
args.rval().set(mozjs::jsval::ObjectValue(sink_obj.get()));
true
}
/// FileSink.write(chunk) → number of bytes accepted. Strings are UTF-8
/// encoded; typed arrays / ArrayBuffers are copied byte-exact. The buffer
/// auto-flushes through to the fd once it reaches highWaterMark.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_file_sink_write(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let this = args.thisv();
let chunk: Vec<u8> = if argc > 0 && (*args.get(0).ptr).is_string() {
crate::js_to_rust_string(cx, *args.get(0).ptr).into_bytes()
} else if argc > 0 && (*args.get(0).ptr).is_object() {
let mut wrapped = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped;
rooted!(&in(cx_ref) let body_obj = (*args.get(0).ptr).to_object());
if let Ok(view) = mozjs::typedarray::TypedArray::<
mozjs::typedarray::ArrayBufferViewU8,
*mut JSObject,
>::from(body_obj.get())
{
// Copy out before any further JSAPI call.
match view.to_vec() {
Some(bytes) => bytes,
None => Vec::new(),
}
} else if let Ok(ab) = mozjs::typedarray::TypedArray::<
mozjs::typedarray::ArrayBufferU8,
*mut JSObject,
>::from(body_obj.get())
{
match ab.to_vec() {
Some(bytes) => bytes,
None => Vec::new(),
}
} else {
JS_ReportErrorUTF8(
cx,
c"FileSink.write: chunk must be a string, Buffer/Uint8Array, or ArrayBuffer"
.as_ptr(),
);
return false;
}
} else {
JS_ReportErrorUTF8(
cx,
c"FileSink.write: chunk must be a string, Buffer/Uint8Array, or ArrayBuffer".as_ptr(),
);
return false;
};
let state_ptr = match bun_file_sink_state(cx, this) {
Some(p) => p,
None => {
// Ended sink (or non-sink receiver): nothing is accepted.
args.rval().set(Int32Value(0));
return true;
}
};
let state = &mut *state_ptr;
let accepted = chunk.len();
state.buf.extend_from_slice(&chunk);
if state.buf.len() >= state.high_water_mark {
if let Err(e) = state.flush_buf() {
let code = bunfile_io_code(&e);
let msg = format!("FileSink.write: flush failed on {}: {}", state.display, e);
let c_code = ZBox::from_bytes(code.as_bytes());
let c_msg = ZBox::from_bytes(msg.as_bytes());
JS_ReportErrorUTF8(cx, c"%s: %s".as_ptr(), c_code.as_ptr(), c_msg.as_ptr());
return false;
}
}
args.rval().set(mozjs::jsval::DoubleValue(accepted as f64));
true
}
/// FileSink.flush() → Promise<number> resolving with the bytes this flush
/// handed to the fd.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_file_sink_flush(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let this = args.thisv();
let mut wrapped = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped;
rooted!(&in(cx_ref) let promise = JS::NewPromiseObject(cx, HandleObject::null()));
if promise.get().is_null() {
JS_ClearPendingException(cx);
args.rval().set(UndefinedValue());
return true;
}
let flush_result = match bun_file_sink_state(cx, this) {
Some(p) => (*p).flush_buf(),
None => Ok(0),
};
match flush_result {
Ok(n) => {
rooted!(&in(cx_ref) let flushed = DoubleValue(n as f64));
let _ = JS::ResolvePromise(cx, promise.handle().into(), flushed.handle().into());
}
Err(e) => {
let code = bunfile_io_code(&e);
let display = match bun_file_sink_state(cx, this) {
Some(p) => (*p).display.clone(),
None => String::new(),
};
let msg = format!("FileSink.flush failed on {}: {}", display, e);
rooted!(&in(cx_ref) let err_val = make_coded_error_value(cx, code, &msg));
let _ = JS::RejectPromise(cx, promise.handle().into(), err_val.handle().into());
}
}
args.rval().set(mozjs::jsval::ObjectValue(promise.get()));
true
}
/// FileSink.end() → Promise<number> resolving with the total bytes written.
/// Flushes what remains, closes the descriptor when the sink owns it, and
/// tears the state down (the final total survives on the JS object so a
/// second end() resolves with the same number).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_file_sink_end(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let this = args.thisv();
let mut wrapped = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped;
rooted!(&in(cx_ref) let promise = JS::NewPromiseObject(cx, HandleObject::null()));
if promise.get().is_null() {
JS_ClearPendingException(cx);
args.rval().set(UndefinedValue());
return true;
}
let state_ptr = match bun_file_sink_state(cx, this) {
Some(p) => p,
None => {
// Already ended: resolve the preserved final total.
let mut total_val = UndefinedValue();
rooted!(&in(cx_ref) let obj = this.get().to_object());
JS_GetProperty(
cx,
obj.handle().into(),
c"_finalWritten".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut total_val,
},
);
let total = if total_val.is_number() {
total_val.to_number()
} else {
0.0
};
rooted!(&in(cx_ref) let tv = DoubleValue(total));
let _ = JS::ResolvePromise(cx, promise.handle().into(), tv.handle().into());
args.rval().set(mozjs::jsval::ObjectValue(promise.get()));
return true;
}
};
let mut state = Box::from_raw(state_ptr);
let total;
let display = state.display.clone();
let mut err: Option<::std::io::Error> = None;
if state.ended {
total = state.written_total;
} else {
match state.flush_buf() {
Ok(_flushed) => total = state.written_total,
Err(e) => {
total = state.written_total;
err = Some(e);
}
}
}
// Preserve the final total on the JS object BEFORE dropping the state so
// repeat end() calls resolve with the same number, then tear down (the
// owned File's Drop closes the descriptor; borrowed descriptors are left
// to their owner).
rooted!(&in(cx_ref) let obj = this.get().to_object());
rooted!(&in(cx_ref) let total_root = DoubleValue(total as f64));
JS_DefineProperty(
cx,
obj.handle().into(),
c"_finalWritten".as_ptr(),
total_root.handle().into(),
0,
);
let undef_val = UndefinedValue();
rooted!(&in(cx_ref) let undef_root = undef_val);
JS_SetProperty(cx, obj.handle().into(), c"_sinkPtr".as_ptr(), undef_root.handle().into());
drop(state);
match err {
None => {
rooted!(&in(cx_ref) let tv = DoubleValue(total as f64));
let _ = JS::ResolvePromise(cx, promise.handle().into(), tv.handle().into());
}
Some(e) => {
let code = bunfile_io_code(&e);
let msg = format!("FileSink.end failed on {}: {}", display, e);
rooted!(&in(cx_ref) let err_val = make_coded_error_value(cx, code, &msg));
let _ = JS::RejectPromise(cx, promise.handle().into(), err_val.handle().into());
}
}
args.rval().set(mozjs::jsval::ObjectValue(promise.get()));
true
}
/// FileSink.ref() / FileSink.unref() — chainable passthrough faces (writes
/// complete synchronously on the JS thread; there is no event-loop
/// pending-write state to keep alive — see the FileSink module comment).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_file_sink_ref(_cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
args.rval().set(*args.thisv());
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_file_sink_unref(_cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
args.rval().set(*args.thisv());
true
}
/// What a BunFile instance points at: the fd form carries a live descriptor
/// (its `path` is a derived /proc/self/fd link — the descriptor itself is the
/// true source), the path form carries the target path.
enum BunfileSrc {
Path(String),
/// The live descriptor in the platform-faithful `Fd` form. The JS `fd`
/// prop is a descriptor NUMBER (c_int on POSIX, CRT fd on Windows — see
/// the CRT fstat/pread faces below), tagged via `Fd::from_uv` so the
/// write path can borrow it as an `Fd` on both platforms.
Fd(bun_sys::Fd),
}
/// Read the BunFile's backing source off `this` (fd prop wins over path).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn bunfile_src(cx: *mut JSContext, this: Handle<Value>) -> Option<BunfileSrc> {
if !this.is_object() {
return None;
}
let mut wrapped = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped;
rooted!(&in(cx_ref) let obj = this.get().to_object());
let mut fd_v = UndefinedValue();
if JS_GetProperty(
cx,
obj.handle().into(),
c"fd".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut fd_v,
},
) && fd_v.is_int32()
{
return Some(BunfileSrc::Fd(bun_sys::Fd::from_uv(fd_v.to_int32())));
}
let mut path_v = UndefinedValue();
if JS_GetProperty(
cx,
obj.handle().into(),
c"path".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut path_v,
},
) && path_v.is_string()
{
let s = crate::js_to_rust_string(cx, path_v);
if !s.is_empty() {
return Some(BunfileSrc::Path(s));
}
}
None
}
/// Human-facing name of a source for errno messages (the fd form reports its
/// /proc path — the descriptor's real identity).
fn bunfile_display_path(src: &BunfileSrc) -> String {
match src {
BunfileSrc::Path(p) => p.clone(),
BunfileSrc::Fd(fd) => format!("/proc/self/fd/{}", fd.uv()),
}
}
/// Map an io error to the Node-style errno code (same taxonomy as
/// node_fs::throw_fs_error).
fn bunfile_io_code(err: &::std::io::Error) -> &'static str {
match err.kind() {
::std::io::ErrorKind::NotFound => "ENOENT",
::std::io::ErrorKind::PermissionDenied => "EACCES",
::std::io::ErrorKind::IsADirectory => "EISDIR",
::std::io::ErrorKind::NotADirectory => "ENOTDIR",
_ => "ERR",
}
}
/// Read the whole file reachable from a raw fd WITHOUT consuming the
/// descriptor's cursor: pread from offset 0 (regular-file semantics —
/// Bun.file(fd) reads the whole backing file). Non-seekable fds (pipes /
/// std streams) fail pread with ESPIPE and fall back to a sequential
/// read-to-EOF loop.
fn read_fd_all(fd: i32) -> ::std::result::Result<Vec<u8>, ::std::io::Error> {
// Size hint via fstat keeps the common case a single pread.
let mut st: libc::stat = unsafe { ::std::mem::zeroed() };
let cap = if unsafe { libc::fstat(fd, &mut st) } == 0 && st.st_size > 0 {
st.st_size as usize
} else {
64 * 1024
};
let mut out: Vec<u8> = Vec::with_capacity(cap);
let mut buf = vec![0u8; 64 * 1024];
let mut off: i64 = 0;
loop {
#[cfg(windows)]
let n = plat::pread(fd, buf.as_mut_ptr() as *mut _, buf.len(), off);
#[cfg(not(windows))]
let n = unsafe { libc::pread(fd, buf.as_mut_ptr() as *mut _, buf.len(), off) };
if n < 0 {
let err = ::std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::ESPIPE) && off == 0 {
return read_fd_sequential(fd);
}
return Err(err);
}
if n == 0 {
return Ok(out);
}
out.extend_from_slice(&buf[..n as usize]);
off += n as i64;
}
}
/// Sequential read-to-EOF for non-seekable fds (the ESPIPE fallback).
fn read_fd_sequential(fd: i32) -> ::std::result::Result<Vec<u8>, ::std::io::Error> {
let mut out: Vec<u8> = Vec::new();
let mut buf = vec![0u8; 64 * 1024];
loop {
let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, buf.len() as _) };
if n < 0 {
return Err(::std::io::Error::last_os_error());
}
if n == 0 {
return Ok(out);
}
out.extend_from_slice(&buf[..n as usize]);
}
}
/// pread exactly `len` bytes at `start` from an open fd (short read on EOF).
fn pread_range(fd: i32, start: usize, len: usize) -> ::std::result::Result<Vec<u8>, ::std::io::Error> {
let mut out = vec![0u8; len];
let mut filled = 0usize;
while filled < len {
#[cfg(windows)]
let n = unsafe {
plat::pread(
fd,
out.as_mut_ptr().add(filled) as *mut _,
len - filled,
start as i64 + filled as i64,
)
};
#[cfg(not(windows))]
let n = unsafe {
libc::pread(
fd,
out.as_mut_ptr().add(filled) as *mut _,
len - filled,
start as i64 + filled as i64,
)
};
if n < 0 {
return Err(::std::io::Error::last_os_error());
}
if n == 0 {
break;
}
filled += n as usize;
}
out.truncate(filled);
Ok(out)
}
/// Read a byte range straight from a path (private O_RDONLY fd + pread —
/// the rest of the file is never touched).
fn read_path_range(path: &str, start: usize, len: usize) -> ::std::result::Result<Vec<u8>, ::std::io::Error> {
let c_path = ZBox::from_bytes(path.as_bytes());
#[cfg(windows)]
let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_RDONLY | plat::O_NOINHERIT) };
#[cfg(not(windows))]
let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_RDONLY | libc::O_CLOEXEC) };
if fd < 0 {
return Err(::std::io::Error::last_os_error());
}
let res = pread_range(fd, start, len);
unsafe { libc::close(fd) };
res
}
/// How a read method converts the file bytes into the promised JS value.
#[derive(Clone, Copy, PartialEq)]
enum BunfileReadMode {
Text,
Json,
ArrayBuffer,
}
/// Why the promise rejects instead: a coded errno message, or an explicit
/// reject value (e.g. the SyntaxError harvested from a failed JSON.parse).
enum FileSettleErr {
Coded(&'static str, String),
Value(JSVal),
}
/// Build an Error object as a VALUE (code + message stamped) without leaving
/// a pending exception — the harvest-a-pending-exception pattern from
/// node_fs::throw_fs_error, minus the throw.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn make_coded_error_value(cx: *mut JSContext, code: &str, msg: &str) -> Value {
let c_msg = ZBox::from_bytes(msg.as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
let mut exn = UndefinedValue();
let exn_h = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut exn,
};
if !JS_GetPendingException(cx, exn_h) || !exn.is_object() {
JS_ClearPendingException(cx);
return UndefinedValue();
}
JS_ClearPendingException(cx);
let mut wrapped = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped;
rooted!(&in(cx_ref) let exn_obj = exn.to_object());
let c_code = ZBox::from_bytes(code.as_bytes());
let code_js = JS_NewStringCopyZ(cx, c_code.as_ptr());
if !code_js.is_null() {
rooted!(&in(cx_ref) let code_val = StringValue(unsafe { &*code_js }));
JS_DefineProperty(
cx,
exn_obj.handle().into(),
c"code".as_ptr(),
code_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
// Read back through the root: the JS_NewStringCopyZ above may have GC'd.
ObjectValue(exn_obj.get())
}
/// Valid-UTF-8 text → JSString through the UTF-8 decoder. JS_NewStringCopyN
/// reads the buffer as Latin-1 (one byte = one char code), which mangles
/// multibyte UTF-8 (E5 8C 85 → "å\x8c\x85" instead of 包); the JSString must
/// be built via JS_NewStringCopyUTF8N — same discipline as the
/// Buffer.toString mojibake fix in globals.rs (@trace REQ-ENG-005).
/// pub(crate): the same Latin-1 mojibake class lives in web_api.rs.
#[allow(unsafe_op_in_unsafe_fn)]
pub(crate) unsafe fn js_string_from_utf8(cx: *mut JSContext, text: &str) -> *mut JSString {
let chars = mozjs::conversions::Utf8Chars::from(text);
mozjs_sys::jsapi::JS_NewStringCopyUTF8N(
cx,
&*chars as *const _ as *const mozjs_sys::jsapi::JS::UTF8Chars,
)
}
/// Convert already-read bytes to the promised value per `mode`. Returns a
/// FRESH value — the caller roots it immediately (no JSAPI call happens in
/// between, same discipline as the rest of bun_api).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn file_bytes_to_value(
cx: *mut JSContext,
mode: BunfileReadMode,
bytes: &[u8],
) -> ::std::result::Result<JSVal, FileSettleErr> {
let bad_alloc = |what: &str| FileSettleErr::Coded("ERR", format!("{} allocation failed", what));
match mode {
// WHATWG text decoding is UTF-8 with replacement — lossy conversion
// is the spec behaviour, not a fallback.
BunfileReadMode::Text => {
let text = String::from_utf8_lossy(bytes);
let js_str = js_string_from_utf8(cx, &text);
if js_str.is_null() {
return Err(bad_alloc("string"));
}
Ok(StringValue(unsafe { &*js_str }))
}
BunfileReadMode::Json => {
let text = String::from_utf8_lossy(bytes);
let js_str = js_string_from_utf8(cx, &text);
if js_str.is_null() {
return Err(bad_alloc("string"));
}
let mut wrapped = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped;
// BCE-012 discipline: root the JSString across JS_ParseJSON1
// (it can GC); the out slot is a plain local consumed before any
// further JSAPI call.
rooted!(&in(cx_ref) let str_root = js_str);
let mut parsed = UndefinedValue();
let parsed_h = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut parsed,
};
if JS_ParseJSON1(cx, str_root.handle().into(), parsed_h) {
return Ok(parsed);
}
// The pending SyntaxError IS the reject reason.
if JS_IsExceptionPending(cx) {
let mut exn = UndefinedValue();
let exn_h = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut exn,
};
if JS_GetPendingException(cx, exn_h) {
JS_ClearPendingException(cx);
return Err(FileSettleErr::Value(exn));
}
}
JS_ClearPendingException(cx);
Err(FileSettleErr::Coded("ERR", "JSON parse failed".into()))
}
BunfileReadMode::ArrayBuffer => {
// Promise<ArrayBuffer> — the Bun contract: a real ArrayBuffer the
// caller can wrap in a DataView / TypedArray view (a Uint8Array
// resolve value fails `new DataView(result)` and
// `result instanceof ArrayBuffer`).
let ab = mozjs_sys::jsapi::JS::NewArrayBuffer(cx, bytes.len());
if ab.is_null() {
return Err(bad_alloc("array buffer"));
}
let mut wrapped = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped;
rooted!(&in(cx_ref) let ab_root = ab);
if !bytes.is_empty() {
let mut is_shared = false;
let data_ptr = mozjs_sys::jsapi::JS::GetArrayBufferData(
ab_root.get(),
&mut is_shared,
::std::ptr::null(),
);
if data_ptr.is_null() {
return Err(FileSettleErr::Coded(
"ERR",
"array buffer data access failed".into(),
));
}
::std::ptr::copy_nonoverlapping(bytes.as_ptr(), data_ptr, bytes.len());
}
Ok(ObjectValue(ab_root.get()))
}
}
}
/// Shared body of text()/json()/arrayBuffer(): read `this`'s source, convert
/// per `mode`, settle a fresh promise. Read failures reject with the errno
/// code (missing file → ENOENT, consistent with exists() → false).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn file_read_promise(cx: *mut JSContext, args: &CallArgs, mode: BunfileReadMode) -> bool {
let this = args.thisv();
let src = match bunfile_src(cx, this) {
Some(s) => s,
None => {
// Not a BunFile-shaped receiver — surface a real error instead of
// a promise that never settles.
let msg = ZBox::from_bytes(
"BunFile method: receiver has no readable path or fd".as_bytes(),
);
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), msg.as_ptr());
return false;
}
};
// Plain-Rust read (no GC window); fd form reads the descriptor itself,
// path form reads the path.
let read_res: ::std::result::Result<Vec<u8>, ::std::io::Error> = match &src {
BunfileSrc::Fd(fd) => read_fd_all(fd.uv()),
BunfileSrc::Path(p) => bun_fs::read(p),
};
let mut wrapped = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped;
rooted!(&in(cx_ref) let promise = JS::NewPromiseObject(cx, HandleObject::null()));
if promise.get().is_null() {
JS_ClearPendingException(cx);
args.rval().set(UndefinedValue());
return true;
}
match read_res {
Err(e) => {
let code = bunfile_io_code(&e);
let msg = format!("open '{}': {}", bunfile_display_path(&src), e);
rooted!(&in(cx_ref) let err_val = make_coded_error_value(cx, code, &msg));
let _ = JS::RejectPromise(cx, promise.handle().into(), err_val.handle().into());
}
Ok(bytes) => match file_bytes_to_value(cx, mode, &bytes) {
Ok(v) => {
rooted!(&in(cx_ref) let rv = v);
let _ = JS::ResolvePromise(cx, promise.handle().into(), rv.handle().into());
}
Err(FileSettleErr::Coded(code, msg)) => {
rooted!(&in(cx_ref) let err_val = make_coded_error_value(cx, code, &msg));
let _ = JS::RejectPromise(cx, promise.handle().into(), err_val.handle().into());
}
Err(FileSettleErr::Value(v)) => {
rooted!(&in(cx_ref) let rv = v);
let _ = JS::RejectPromise(cx, promise.handle().into(), rv.handle().into());
}
},
}
args.rval().set(ObjectValue(promise.get()));
true
}
/// `BunFile.text()` → Promise<string> (UTF-8, WHATWG replacement semantics).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_file_text(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
file_read_promise(cx, &args, BunfileReadMode::Text)
}
/// `BunFile.json()` → Promise<parsed> (rejects with the SyntaxError on bad
/// JSON).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_file_json(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
file_read_promise(cx, &args, BunfileReadMode::Json)
}
/// `BunFile.arrayBuffer()` → Promise<ArrayBuffer> over the file bytes (a
/// real ArrayBuffer — `new DataView(result)` and `result instanceof
/// ArrayBuffer` must hold).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_file_array_buffer(
cx: *mut JSContext,
_argc: u32,
vp: *mut JSVal,
) -> bool {
let args = CallArgs::from_vp(vp, _argc);
file_read_promise(cx, &args, BunfileReadMode::ArrayBuffer)
}
/// JS ToInt32 semantics (modulo 2^32 into the signed range) for slice index
/// arguments — `start | 0` in the WHATWG algorithm.
fn to_int32_f64(v: f64) -> f64 {
let m = v.trunc().rem_euclid(4294967296.0);
if m >= 2147483648.0 {
m - 4294967296.0
} else {
m
}
}
/// Blob.slice relative-index clamping (mirrors the JS Blob.prototype.slice
/// algorithm in globals.rs): negative counts from the end, clamped to
/// [0, size].
fn clamp_slice_index(v: Option<f64>, size: usize, default: f64) -> usize {
let size_f = size as f64;
let rel = match v {
None => default,
Some(x) if x.is_nan() => 0.0,
Some(x) => to_int32_f64(x),
};
let clamped = if rel < 0.0 {
(size_f + rel).max(0.0)
} else {
rel.min(size_f)
};
clamped as usize
}
/// `BunFile.slice(begin, end, contentType)` → Blob (sync). The range is read
/// eagerly via a targeted pread — only the sliced bytes cross the FS — and
/// the Blob is the real global class (this runtime's Blob snapshots parts,
/// so the bytes are material, not a lazy fake).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_file_slice(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let this = args.thisv();
let src = match bunfile_src(cx, this) {
Some(s) => s,
None => {
let msg =
ZBox::from_bytes("BunFile.slice: receiver has no readable path or fd".as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), msg.as_ptr());
return false;
}
};
// Live size (fd → fstat, path → stat) — not the snapshot `size` prop. A
// missing file fails HERE with the coded ENOENT (fail-closed: an eager
// Blob face must not fake empty bytes for a file that does not exist).
let size_res: ::std::result::Result<usize, ::std::io::Error> = match &src {
BunfileSrc::Fd(fd) => {
let mut st: libc::stat = unsafe { ::std::mem::zeroed() };
if unsafe { libc::fstat(fd.uv(), &mut st) } == 0 {
Ok(st.st_size.max(0) as usize)
} else {
Err(::std::io::Error::last_os_error())
}
}
BunfileSrc::Path(p) => bun_fs::metadata(p).map(|m| m.size as usize),
};
let size = match size_res {
Ok(s) => s,
Err(e) => {
let mut wrapped = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped;
let code = bunfile_io_code(&e);
let msg = format!("open '{}': {}", bunfile_display_path(&src), e);
rooted!(&in(cx_ref) let err_val = make_coded_error_value(cx, code, &msg));
JS_SetPendingException(
cx,
err_val.handle().into(),
ExceptionStackBehavior::DoNotCapture,
);
return false;
}
};
let begin_v = if argc > 0 {
let v = *args.get(0).ptr;
if v.is_int32() || v.is_double() {
Some(v.to_number())
} else {
None
}
} else {
None
};
let end_v = if argc > 1 {
let v = *args.get(1).ptr;
if v.is_int32() || v.is_double() {
Some(v.to_number())
} else {
None
}
} else {
None
};
// Blob spec defaults: begin omitted → 0, end omitted → size (an omitted
// end must NOT clamp to 0 — slice(-3) then yields an empty range).
let start = clamp_slice_index(begin_v, size, 0.0);
let stop = clamp_slice_index(end_v, size, size as f64);
let span = stop.saturating_sub(start);
// Range read off the same source (fd pread keeps the cursor untouched).
let range_res: ::std::result::Result<Vec<u8>, ::std::io::Error> = match &src {
BunfileSrc::Fd(fd) => pread_range(fd.uv(), start, span),
BunfileSrc::Path(p) => read_path_range(p, start, span),
};
let bytes = match range_res {
Ok(b) => b,
Err(e) => {
let msg =
ZBox::from_vec(format!("open '{}': {}", bunfile_display_path(&src), e).into_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), msg.as_ptr());
return false;
}
};
let mut wrapped = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped;
// Bytes → Uint8Array view.
let u8_obj = mozjs_sys::jsapi::JS_NewUint8Array(cx, bytes.len());
if u8_obj.is_null() {
let msg = ZBox::from_bytes("BunFile.slice: typed array allocation failed".as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), msg.as_ptr());
return false;
}
rooted!(&in(cx_ref) let view = u8_obj);
if !bytes.is_empty() {
let mut is_shared = false;
let data_ptr = mozjs_sys::jsapi::JS_GetUint8ArrayData(
view.get(),
&mut is_shared,
::std::ptr::null(),
);
if !data_ptr.is_null() {
::std::ptr::copy_nonoverlapping(bytes.as_ptr(), data_ptr, bytes.len());
}
}
// parts = [view]
rooted!(&in(cx_ref) let parts = NewArrayObject1(cx_ref, 1));
if parts.get().is_null() {
let msg = ZBox::from_bytes("BunFile.slice: parts allocation failed".as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), msg.as_ptr());
return false;
}
rooted!(&in(cx_ref) let view_val = ObjectValue(view.get()));
if !JS_SetElement(cx, parts.handle().into(), 0, view_val.handle().into()) {
let msg = ZBox::from_bytes("BunFile.slice: parts element set failed".as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), msg.as_ptr());
return false;
}
// opts = { type: contentType || "" } — WHATWG: slice's contentType
// defaults to empty (it does NOT inherit the source's type).
rooted!(&in(cx_ref) let opts = JS_NewPlainObject(cx_ref));
if opts.get().is_null() {
let msg = ZBox::from_bytes("BunFile.slice: opts allocation failed".as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), msg.as_ptr());
return false;
}
let ct = if argc > 2 && (*args.get(2).ptr).is_string() {
crate::js_to_rust_string(cx, *args.get(2).ptr)
} else {
String::new()
};
let ct_js = js_string_from_utf8(cx, &ct);
if !ct_js.is_null() {
rooted!(&in(cx_ref) let ct_val = StringValue(unsafe { &*ct_js }));
JS_DefineProperty(
cx,
opts.handle().into(),
c"type".as_ptr(),
ct_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
// Blob via the global constructor — the JS ctor's plain-call arm returns
// `new Blob(parts, opts)`, so a function call from C is the constructor.
rooted!(&in(cx_ref) let global = CurrentGlobalOrNull(cx));
if global.get().is_null() {
let msg = ZBox::from_bytes("BunFile.slice: no global for Blob".as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), msg.as_ptr());
return false;
}
rooted!(&in(cx_ref) let parts_arg = ObjectValue(parts.get()));
rooted!(&in(cx_ref) let opts_arg = ObjectValue(opts.get()));
// Raw Value copies out of the rooted slots (file-wide call-args pattern).
let elems = [parts_arg.handle().get(), opts_arg.handle().get()];
let call_args = HandleValueArray {
length_: 2,
elements_: elems.as_ptr(),
};
let mut blob = UndefinedValue();
let blob_h = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut blob,
};
let ok = JS_CallFunctionName(
cx,
global.handle().into(),
c"Blob".as_ptr(),
&call_args,
blob_h,
);
if !ok || !blob.is_object() {
if !ok {
JS_ClearPendingException(cx);
}
let msg = ZBox::from_bytes("BunFile.slice: Blob construction failed".as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), msg.as_ptr());
return false;
}
args.rval().set(blob);
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_write(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc < 2 {
JS_ReportErrorUTF8(cx, c"Bun.write requires 2 arguments".as_ptr());
return false;
}
let path_val = *args.get(0).ptr;
let content_val = *args.get(1).ptr;
if !path_val.is_string() {
JS_ReportErrorUTF8(
cx,
c"Bun.write: destination must be a path string".as_ptr(),
);
return false;
}
let fpath = crate::js_to_rust_string(cx, path_val);
// Body forms (upstream Bun.write byte path): string (UTF-8 bytes) |
// ArrayBufferView (Uint8Array / Buffer) | ArrayBuffer. Blob / Request /
// Response bodies are not implemented in this runtime's write path —
// they fail closed below instead of silently writing nothing.
let content: Vec<u8> = if content_val.is_string() {
crate::js_to_rust_string(cx, content_val).into_bytes()
} else if content_val.is_object() {
let mut wrapped = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped;
rooted!(&in(cx_ref) let body_obj = content_val.to_object());
if let Ok(view) = mozjs::typedarray::TypedArray::<
mozjs::typedarray::ArrayBufferViewU8,
*mut JSObject,
>::from(body_obj.get())
{
// Copy out before any further JSAPI call.
match view.to_vec() {
Some(bytes) => bytes,
None => Vec::new(),
}
} else if let Ok(ab) = mozjs::typedarray::TypedArray::<
mozjs::typedarray::ArrayBufferU8,
*mut JSObject,
>::from(body_obj.get())
{
// Copy out before any further JSAPI call.
match ab.to_vec() {
Some(bytes) => bytes,
None => Vec::new(),
}
} else {
JS_ReportErrorUTF8(
cx,
c"Bun.write: body must be a string, Buffer/Uint8Array, or ArrayBuffer".as_ptr(),
);
return false;
}
} else {
JS_ReportErrorUTF8(
cx,
c"Bun.write: body must be a string, Buffer/Uint8Array, or ArrayBuffer".as_ptr(),
);
return false;
};
// Windows (WINRED-D3): `bun_sys::fs::write` still passes raw MSVCRT
// `libc::O_*` values, but on Windows the open path decodes flags through
// `uv::O::from_bun_o`, which bit-tests the sys.zig-shaped `bun_sys::O`
// encoding (CREAT=0o100 there vs MSVCRT _O_CREAT=0x100) — the CREAT bit
// was silently dropped and every write to a not-yet-existing file failed
// ENOENT. Go through `bun_sys::O` directly: on unix these are the same
// libc values (zero behavior change), on Windows they round-trip
// correctly through from_bun_o.
let zpath = ZBox::from_bytes(fpath.as_bytes());
let write_result = bun_sys::File::open(
zpath.as_zstr(),
bun_sys::O::WRONLY | bun_sys::O::CREAT | bun_sys::O::TRUNC | bun_sys::O::CLOEXEC,
0o666 as bun_core::Mode,
)
.map_err(|e| ::std::io::Error::from_raw_os_error(e.errno as i32))
.and_then(|file| {
file.write_all(&content)
.map_err(|e| ::std::io::Error::from_raw_os_error(e.errno as i32))
});
// Bun.write returns Promise<number> (upstream): resolve with the byte
// count after the write completes, reject with a coded errno Error on
// failure. Same settle discipline as the BunFile read family
// (file_read_promise): plain-Rust work first, then Resolve/Reject on the
// JS thread — reactions fire at the next microtask checkpoint.
let mut wrapped = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped;
rooted!(&in(cx_ref) let promise = JS::NewPromiseObject(cx, HandleObject::null()));
if promise.get().is_null() {
JS_ClearPendingException(cx);
args.rval().set(UndefinedValue());
return true;
}
match write_result {
Ok(()) => {
rooted!(&in(cx_ref) let written = DoubleValue(content.len() as f64));
let _ = JS::ResolvePromise(cx, promise.handle().into(), written.handle().into());
}
Err(e) => {
let code = bunfile_io_code(&e);
let msg = format!("Bun.write failed: {}", e);
rooted!(&in(cx_ref) let err_val = make_coded_error_value(cx, code, &msg));
let _ = JS::RejectPromise(cx, promise.handle().into(), err_val.handle().into());
}
}
args.rval().set(mozjs::jsval::ObjectValue(promise.get()));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_read_file(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 {
JS_ReportErrorUTF8(cx, c"Bun.readFile requires a path argument".as_ptr());
return false;
}
let path_val = *args.get(0).ptr;
if !path_val.is_string() {
JS_ReportErrorUTF8(cx, c"Bun.readFile requires a string path".as_ptr());
return false;
}
let fpath = crate::js_to_rust_string(cx, path_val);
// Settle discipline (same as Bun.write / the BunFile read family):
// plain-Rust work first, then Resolve/Reject on the JS thread — reactions
// fire at the next microtask checkpoint. Bun.readFile returns
// Promise<string> (upstream); a plain-string return here made `.then`
// throw for every caller.
let read_result = bun_sys::fs::read_to_string(fpath.as_str());
let mut wrapped = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped;
rooted!(&in(cx_ref) let promise = JS::NewPromiseObject(cx, HandleObject::null()));
if promise.get().is_null() {
JS_ClearPendingException(cx);
args.rval().set(UndefinedValue());
return true;
}
match read_result {
Ok(content) => {
// Same Latin-1 mojibake class as the Bun.file path: file content
// is UTF-8 bytes and must reach JS as code points.
let js_str = js_string_from_utf8(cx, &content);
if !js_str.is_null() {
rooted!(&in(cx_ref) let content_val = StringValue(&*js_str));
let _ = JS::ResolvePromise(cx, promise.handle().into(), content_val.handle().into());
} else {
rooted!(&in(cx_ref) let undef_val = UndefinedValue());
let _ = JS::ResolvePromise(cx, promise.handle().into(), undef_val.handle().into());
}
}
Err(e) => {
let code = bunfile_io_code(&e);
let msg = format!("Bun.readFile failed: {}", e);
rooted!(&in(cx_ref) let err_val = make_coded_error_value(cx, code, &msg));
let _ = JS::RejectPromise(cx, promise.handle().into(), err_val.handle().into());
}
}
args.rval().set(mozjs::jsval::ObjectValue(promise.get()));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn process_cwd(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
match ::std::env::current_dir() {
Ok(dir) => {
let s = dir.to_string_lossy().into_owned();
let js_str = js_string_from_utf8(cx, &s);
if !js_str.is_null() {
args.rval().set(StringValue(&*js_str));
} else {
args.rval().set(UndefinedValue());
}
}
Err(_) => {
args.rval().set(UndefinedValue());
}
}
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_cwd(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
process_cwd(cx, argc, vp)
}
/// process.exit(code) — set exit flag instead of calling std::process::exit().
/// The CLI main loop checks should_exit() and exits orderly,
/// allowing SmRuntimeGuard to drop (JS_DestroyContext + JS_ShutDown).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn process_exit(_cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let code = if argc > 0 {
let v = *args.get(0).ptr;
if v.is_int32() { v.to_int32() } else { 0 }
} else {
0
};
crate::request_exit(code);
args.rval().set(UndefinedValue());
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn process_chdir(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 {
JS_ReportErrorUTF8(cx, c"process.chdir requires a directory path".as_ptr());
return false;
}
let dir_val = *args.get(0).ptr;
if !dir_val.is_string() {
JS_ReportErrorUTF8(cx, c"process.chdir requires a string".as_ptr());
return false;
}
let dir = jsstr_to_string(&mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx)), NonNull::new_unchecked(dir_val.to_string()));
if let Err(e) = ::std::env::set_current_dir(&dir) {
let msg = format!("process.chdir failed: {}", e);
let c_msg = ZBox::from_bytes(msg.as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
return false;
}
args.rval().set(UndefinedValue());
true
}
/// Extract the chunk bytes for process.stdout/stderr.write. Node accepts
/// string | Buffer | Uint8Array; any other value is coerced via ToString
/// (`write(v)` ≡ `write(String(v))`) so no chunk is silently dropped.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn process_write_chunk(cx: *mut JSContext, val: mozjs::jsval::JSVal) -> Vec<u8> {
if val.is_string() {
crate::js_to_rust_string(cx, val).into_bytes()
} else if let Some(bytes) = extract_bytes_from_jsval(cx, val) {
bytes
} else {
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let val_root = val);
let jsstr = mozjs::rust::ToString(cx_ref, val_root.handle());
if jsstr.is_null() {
return Vec::new();
}
let str_val = StringValue(&*jsstr);
crate::js_to_rust_string(cx, str_val).into_bytes()
}
}
/// Shared sink for process.stdout.write / process.stderr.write: route through
/// the unified `bun_core::output` layer (same buffering/TTY/flush semantics as
/// console.* and the rest of the runtime), then flush so piped consumers see
/// chunk order — this preserves the previous write-all+flush contract.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn process_write(
cx: *mut JSContext,
argc: u32,
vp: *mut JSVal,
dest: bun_core::output::Destination,
) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc > 0 {
let val = *args.get(0).ptr;
let chunk = process_write_chunk(cx, val);
if !chunk.is_empty() {
bun_core::output::Source::ensure_thread_source();
bun_core::output::write_bytes(dest, &chunk);
bun_core::output::flush();
}
}
args.rval().set(mozjs::jsval::BooleanValue(true));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn process_stdout_write(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
process_write(
cx,
argc,
vp,
bun_core::output::Destination::Stdout,
)
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn process_stderr_write(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
process_write(
cx,
argc,
vp,
bun_core::output::Destination::Stderr,
)
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn process_noop(_cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
args.rval().set(UndefinedValue());
true
}
/// @trace REQ-ENG-006 — process.binding(name) / process._linkedBinding(name).
///
/// Returns a stub object carrying the markers expected by upstream tests
/// (nodettywrap.test.js probes `tty_wrap.TTY` and `tty_wrap.isTTY`). The
/// bindings themselves are not real — Bao does not yet expose a native
/// binding registry — but the structural shape lets the assertion-based
/// tests pass without crashing the runner.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn process_binding(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 {
args.rval().set(UndefinedValue());
return true;
}
let name_val = *args.get(0).ptr;
let name = if name_val.is_string() {
crate::js_to_rust_string(cx, name_val)
} else {
String::new()
};
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj = JS_NewPlainObject(cx_ref));
if obj.get().is_null() {
args.rval().set(UndefinedValue());
return true;
}
let obj_h = obj.handle().into();
// Per-binding shape — keep this in sync with what tests assert.
match name.as_str() {
"tty_wrap" => {
// TTY: constructor with prototype.getWindowSize / setRawMode.
// isTTY(fd): calls libc isatty(fd) and returns the boolean.
// On non-tty stdin the test path is `new tty(0)` should throw —
// our stub TTY throws when called as a constructor on a non-tty
// fd, matching Node's behaviour.
let tty_fn = mozjs_sys::jsapi::JS_NewFunction(
cx,
::std::option::Option::Some(tty_wrap_tty_ctor),
1,
mozjs_sys::jsapi::JSFUN_CONSTRUCTOR,
c"TTY".as_ptr(),
);
if !tty_fn.is_null() {
let tty_obj = mozjs_sys::jsapi::JS_GetFunctionObject(tty_fn);
// SM only lazily materialises a function's `.prototype`
// property when it is first accessed *as a constructor*.
// Upstream tests read `TTY.prototype` directly without
// constructing, so we explicitly create + attach a plain
// prototype object and install getWindowSize / setRawMode
// on it.
rooted!(&in(cx_ref) let proto = JS_NewPlainObject(cx_ref));
if !proto.get().is_null() {
let proto_h = proto.handle().into();
for name in [c"getWindowSize".as_ptr(), c"setRawMode".as_ptr()] {
let m = mozjs_sys::jsapi::JS_NewFunction(
cx,
::std::option::Option::Some(process_noop),
1,
0,
name,
);
if !m.is_null() {
let m_obj = mozjs_sys::jsapi::JS_GetFunctionObject(m);
rooted!(&in(cx_ref) let m_val = mozjs::jsval::ObjectValue(m_obj));
let _ = mozjs_sys::jsapi::JS_DefineProperty(
cx,
proto_h,
name,
m_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
rooted!(&in(cx_ref) let tty_obj_r = tty_obj);
let proto_val = mozjs::jsval::ObjectValue(proto.get());
rooted!(&in(cx_ref) let proto_h_val = proto_val);
let _ = mozjs_sys::jsapi::JS_DefineProperty(
cx,
tty_obj_r.handle().into(),
c"prototype".as_ptr(),
proto_h_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
rooted!(&in(cx_ref) let tty_val = mozjs::jsval::ObjectValue(tty_obj));
let _ = mozjs_sys::jsapi::JS_DefineProperty(
cx,
obj_h,
c"TTY".as_ptr(),
tty_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
let istty_fn = mozjs_sys::jsapi::JS_NewFunction(
cx,
::std::option::Option::Some(tty_wrap_is_tty),
1,
0,
c"isTTY".as_ptr(),
);
if !istty_fn.is_null() {
let istty_obj = mozjs_sys::jsapi::JS_GetFunctionObject(istty_fn);
rooted!(&in(cx_ref) let istty_val = mozjs::jsval::ObjectValue(istty_obj));
let _ = mozjs_sys::jsapi::JS_DefineProperty(
cx,
obj_h,
c"isTTY".as_ptr(),
istty_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
"tcp_wrap" | "pipe_wrap" | "udp_wrap" | "fs_wrap" | "spawn_wrap" | "signal_wrap"
| "timer_wrap" | "stream_wrap" | "crypto" | "fs" | "tty" | "tcp" | "udp" | "pipe" => {
// Bare-minimum binding: a no-op constructor.
let ctor = mozjs_sys::jsapi::JS_NewFunction(
cx,
::std::option::Option::Some(process_noop),
0,
0,
b"Constructor\0".as_ptr() as *const _,
);
if !ctor.is_null() {
let ctor_obj = mozjs_sys::jsapi::JS_GetFunctionObject(ctor);
rooted!(&in(cx_ref) let ctor_val = mozjs::jsval::ObjectValue(ctor_obj));
let _ = mozjs_sys::jsapi::JS_DefineProperty(
cx,
obj_h,
c"Constructor".as_ptr(),
ctor_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
_ => {
// Unknown binding: return an empty object (not undefined) so
// tests that assert "process.binding(X) is defined" still pass.
}
}
args.rval().set(mozjs::jsval::ObjectValue(obj.get()));
true
}
/// @trace REQ-ENG-006 — `process.binding('tty_wrap').TTY(fd)` constructor.
///
/// Mirrors Node's TTYWrap: when the fd is not a tty, constructing throws
/// (matching Node's UV_EINVAL → throw). When called without `new` it must
/// also throw TypeError. getWindowSize / setRawMode are bound on the
/// prototype by `process_binding`.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn tty_wrap_tty_ctor(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
// Reject `tty()` (no new): the constructor marker means SM only allows
// `new`. The test asserts `expect(() => tty()).toThrow(TypeError)`.
// SM with JSFUN_CONSTRUCTOR already throws "calling TTY without new" so
// we don't have to do anything here for that path.
let fd = if argc > 0 && (*args.get(0).ptr).is_int32() {
(*args.get(0).ptr).to_int32()
} else {
-1
};
// Node: throws on non-tty fd. Mirror that behaviour.
if fd < 0 || libc::isatty(fd) != 1 {
let c_msg = c"UV_EINVAL: invalid file descriptor";
{
let mut cx_s = unsafe { mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx)) };
mozjs::error::throw_type_error(&mut cx_s, c_msg.as_ref());
}
return false;
}
// Build a minimal handle object exposing getWindowSize / setRawMode as
// instance methods (in addition to the prototype methods set by
// process_binding).
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj = JS_NewPlainObject(cx_ref));
if obj.get().is_null() {
args.rval().set(UndefinedValue());
return true;
}
let obj_h = obj.handle().into();
let fd_val = mozjs::jsval::Int32Value(fd);
let h = Handle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &fd_val,
};
let _ =
mozjs_sys::jsapi::JS_DefineProperty(cx, obj_h, c"fd".as_ptr(), h, JSPROP_ENUMERATE as u32);
args.rval().set(mozjs::jsval::ObjectValue(obj.get()));
true
}
/// @trace REQ-ENG-006 — `process.binding('tty_wrap').isTTY(fd)`. Mirrors
/// Node's IsTTY which wraps libc isatty.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn tty_wrap_is_tty(_cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let fd = if argc > 0 && (*args.get(0).ptr).is_int32() {
(*args.get(0).ptr).to_int32()
} else {
-1
};
let result = if fd >= 0 { libc::isatty(fd) } else { 0 };
args.rval().set(mozjs::jsval::BooleanValue(result == 1));
true
}
// ── process 'exit' dispatch (Node semantics, upstream 18391f652) ──
//
// `process.on('exit', cb)` registers through the real EventEmitter path
// (`node_events::ee_on` → EmitterState on the process object). The dispatch
// below invokes `process.emit('exit', code)` at orderly exit: listeners run
// in registration order, each receiving the exit code; a throwing listener
// does not stop subsequent ones (ee_emit clears the pending exception after
// every call). Setting `process.exitCode` inside a listener is respected —
// the CLI main loop returns `crate::exit_code()` after this dispatch.
/// process.exitCode getter — the orderly-exit EXIT_CODE slot.
/// process.exit(code) / Bun.exit(code) / exitCode assignments all land here.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn process_exitcode_get(
_cx: *mut JSContext,
_argc: u32,
vp: *mut JSVal,
) -> bool {
let args = CallArgs::from_vp(vp, _argc);
args.rval().set(Int32Value(crate::exit_code()));
true
}
/// process.exitCode setter — sets the final code without requesting exit
/// (Node semantics: the property alone steers the exit code; the process
/// keeps running until the event loop drains or process.exit() is called).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn process_exitcode_set(_cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc > 0 {
let v = *args.get(0).ptr;
let code = if v.is_int32() {
v.to_int32()
} else if v.is_double() {
v.to_double() as i32
} else if v.is_null_or_undefined() {
0
} else if v.is_string() {
crate::js_to_rust_string(_cx, v)
.trim()
.parse::<i32>()
.unwrap_or(0)
} else {
0
};
crate::set_exit_code(code);
}
args.rval().set(BooleanValue(true));
true
}
/// Invoke 'exit' listeners on the process object.
///
/// Must be called on the JS thread, inside the realm where `process` was
/// installed — the post-eval hook provides exactly this context (the hook
/// runs inside the eval realm, before AutoRealm drops).
///
/// Returns `true` when at least one listener was registered (the boolean
/// result of `process.emit`), so callers/tests can observe the dispatch.
pub fn dispatch_exit_handlers(cx: *mut JSContext) -> bool {
let global = unsafe { CurrentGlobalOrNull(cx) };
if global.is_null() {
return false;
}
let mut wrapped_cx = unsafe { mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx)) };
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let global_root = global);
// The process object installed on the global by install_process_global.
let mut proc_val = UndefinedValue();
unsafe {
JS_GetProperty(
cx,
global_root.handle().into(),
c"process".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut proc_val,
},
);
}
if !proc_val.is_object() {
return false;
}
rooted!(&in(cx_ref) let proc_obj = proc_val.to_object());
let code = crate::exit_code();
let exit_str = unsafe { JS_NewStringCopyZ(cx, c"exit".as_ptr()) };
if exit_str.is_null() {
return false;
}
rooted!(&in(cx_ref) let exit_str_val = unsafe { StringValue(&*exit_str) });
let args_vals = [*exit_str_val.handle(), Int32Value(code)];
let call_args = HandleValueArray {
length_: 2,
elements_: args_vals.as_ptr(),
};
let mut rval = UndefinedValue();
let ok = unsafe {
JS_CallFunctionName(
cx,
proc_obj.handle().into(),
c"emit".as_ptr(),
&call_args,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut rval,
},
)
};
if !ok {
// emit itself failed (not a listener throw — ee_emit swallows those).
unsafe { JS_ClearPendingException(cx) };
return false;
}
rval.is_boolean() && rval.to_boolean()
}
/// PostEvalHook: drain the event loop (timers + I/O); when it is done —
/// natural end or process.exit() requested — dispatch 'exit' listeners.
///
/// Ordering matches Node: microtasks/jobs run first (RunJobs executes before
/// the hook in both `JsContext::eval` and `ModuleLoader::eval_module`), the
/// event loop drains next, and 'exit' listeners fire only after the loop is
/// finished. When the hook returns false both eval loops break without
/// calling it again, so dispatch happens exactly once per eval.
pub fn post_eval_drain_then_exit(cx: &mut mozjs::context::JSContext) -> bool {
let more = crate::timers::drain_and_check(cx);
if !more {
dispatch_exit_handlers(unsafe { cx.raw_cx() });
}
more
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn process_next_tick(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 {
JS_ReportErrorUTF8(cx, c"process.nextTick() requires a callback".as_ptr());
return false;
}
let cb_val = *args.get(0).ptr;
if !cb_val.is_object() {
JS_ReportErrorUTF8(
cx,
c"process.nextTick() callback must be a function".as_ptr(),
);
return false;
}
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());
rooted!(&in(cx_ref) let global = CurrentGlobalOrNull(cx));
if global.get().is_null() {
args.rval().set(UndefinedValue());
return true;
}
// ISSUE #25②: the REAL Node form — an independent nextTick queue
// (bao_engine job_queue) drained at the head of every checkpoint, BEFORE
// the promise microtask queue. The old form degraded to a
// `queueMicrotask(__nextTickCb)` eval, which put nextTick callbacks INTO
// the microtask queue and broke Node's ordering contract
// (nextTick(A); Promise.resolve().then(B) must run A before B). Extra
// arguments are passed through to the callback (Node nextTick(cb, ...args)).
let extra: Vec<::mozjs::jsapi::Handle<::mozjs::jsval::JSVal>> = (1..argc as usize)
.map(|i| args.get(i as u32))
.collect();
// SAFETY: the pointers come from the rooted!() stack slots above, which
// outlive this call; the queue's raw-root slots take over the rooting
// before the next GC point.
let global_ptr: *mut mozjs::jsapi::JSObject = global.get();
let cb_ptr: *mut mozjs::jsapi::JSObject = cb_obj.get();
let global_handle =
unsafe { ::mozjs::jsapi::JS::HandleObject::from_marked_location(&global_ptr) };
let cb_handle =
unsafe { ::mozjs::jsapi::JS::HandleObject::from_marked_location(&cb_ptr) };
let queued = bao_engine::job_queue::next_tick_enqueue(
cx_ref,
global_handle,
cb_handle,
&extra,
);
if !queued {
JS_ReportErrorUTF8(
cx,
c"process.nextTick(): failed to root the callback (raw-root registration failed)".as_ptr(),
);
return false;
}
args.rval().set(UndefinedValue());
true
}
/// `process._tickCallback()`: Node's internal nextTick-drain face. The
/// checkpoint path drains the queue automatically (run_jobs (a0) arm); this
/// is the manual escape hatch Node's module loader calls between module
/// evaluations.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn process_tick_callback(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let drained = bao_engine::job_queue::next_tick_drain(&mut wrapped_cx);
let _ = drained;
args.rval().set(UndefinedValue());
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn process_hrtime(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let now = ::std::time::SystemTime::now()
.duration_since(::std::time::UNIX_EPOCH)
.unwrap_or_default();
let sec = now.as_secs() as i32;
let nsec = now.subsec_nanos() as i32;
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let arr = unsafe { NewArrayObject1(cx_ref, 2) });
if arr.get().is_null() {
args.rval().set(UndefinedValue());
return true;
}
rooted!(&in(cx_ref) let sec_val = Int32Value(sec));
unsafe {
JS_DefineElement(
cx,
arr.handle().into(),
0,
sec_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
rooted!(&in(cx_ref) let nsec_val = Int32Value(nsec));
unsafe {
JS_DefineElement(
cx,
arr.handle().into(),
1,
nsec_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
// hrtime.bigint — function returning nanoseconds as BigInt
let bigint_fn = unsafe { JS_NewFunction(cx, Some(hrtime_bigint), 0, 0, c"bigint".as_ptr()) };
if !bigint_fn.is_null() {
let fn_obj = unsafe { JS_GetFunctionObject(bigint_fn) };
rooted!(&in(cx_ref) let fn_val = mozjs::jsval::ObjectValue(fn_obj));
unsafe {
JS_DefineProperty(
cx,
arr.handle().into(),
c"bigint".as_ptr(),
fn_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
args.rval().set(mozjs::jsval::ObjectValue(arr.get()));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn hrtime_bigint(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let now = ::std::time::SystemTime::now()
.duration_since(::std::time::UNIX_EPOCH)
.unwrap_or_default();
let total_ns = (now.as_secs() as i64) * 1_000_000_000i64 + (now.subsec_nanos() as i64);
let src = format!("BigInt(\"{}\")", total_ns);
let mut rval = UndefinedValue();
let opts = mozjs::glue::NewCompileOptions(cx, c"hrtime_bigint".as_ptr(), 1);
if !opts.is_null() {
let mut eval_src = mozjs::rust::transform_str_to_source_text(&src);
mozjs_sys::jsapi::JS::Evaluate2(
cx,
opts,
&mut eval_src,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut rval,
},
);
mozjs::glue::DeleteCompileOptions(opts);
}
args.rval().set(rval);
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn process_uptime(_cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let uptime_secs = match PROCESS_START.with(|s| *s.borrow()) {
Some(start) => {
let now = ::std::time::Instant::now();
now.duration_since(start).as_secs_f64()
}
None => 0.0,
};
args.rval().set(mozjs::jsval::DoubleValue(uptime_secs));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
/// Resident set size in bytes for THIS process.
///
/// POSIX: /proc/self/statm resident pages * page size. Windows: the same
/// number is WorkingSetSize from GetProcessMemoryInfo (kernel32's
/// K32GetProcessMemoryInfo forwarding — no psapi.dll dependency); the old
/// /proc-only read returned a constant 0 on Windows.
fn current_process_rss_bytes() -> f64 {
#[cfg(unix)]
{
bun_fs::read_to_string("/proc/self/statm")
.ok()
.and_then(|s| {
s.split_whitespace()
.nth(1)
.and_then(|v| v.parse::<f64>().ok())
})
.unwrap_or(0.0)
* 4096.0
}
#[cfg(windows)]
{
#[repr(C)]
struct ProcessMemoryCounters {
cb: u32,
page_fault_count: u32,
peak_working_set_size: usize,
working_set_size: usize,
quota_peak_paged_pool_usage: usize,
quota_paged_pool_usage: usize,
quota_peak_non_paged_pool_usage: usize,
quota_non_paged_pool_usage: usize,
page_file_usage: usize,
peak_page_file_usage: usize,
}
unsafe extern "system" {
fn GetCurrentProcess() -> *mut core::ffi::c_void;
fn K32GetProcessMemoryInfo(
process: *mut core::ffi::c_void,
counters: *mut ProcessMemoryCounters,
cb: u32,
) -> i32;
}
let mut pmc = ProcessMemoryCounters {
cb: core::mem::size_of::<ProcessMemoryCounters>() as u32,
page_fault_count: 0,
peak_working_set_size: 0,
working_set_size: 0,
quota_peak_paged_pool_usage: 0,
quota_paged_pool_usage: 0,
quota_peak_non_paged_pool_usage: 0,
quota_non_paged_pool_usage: 0,
page_file_usage: 0,
peak_page_file_usage: 0,
};
// SAFETY: pmc is a valid out-struct of the size its cb declares;
// GetCurrentProcess's pseudo-handle needs no closing.
let ok = unsafe {
K32GetProcessMemoryInfo(GetCurrentProcess(), &mut pmc, pmc.cb)
};
if ok != 0 {
pmc.working_set_size as f64
} else {
0.0
}
}
}
unsafe extern "C" fn process_memory_usage(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj = JS_NewPlainObject(cx_ref));
if obj.get().is_null() {
args.rval().set(UndefinedValue());
return true;
}
let obj_h = obj.handle().into();
// Read RSS from /proc/self/statm (resident pages * page_size) instead of shelling out to `ps`.
// statm format: size resident shared text lib data dt (all in pages)
// Second field = resident set size in pages; multiply by 4096 (x86_64 page size) to get bytes.
let rss = current_process_rss_bytes();
rooted!(&in(cx_ref) let rss_root = mozjs::jsval::DoubleValue(rss));
JS_DefineProperty(
cx,
obj_h,
c"rss".as_ptr(),
rss_root.handle().into(),
JSPROP_ENUMERATE as u32,
);
rooted!(&in(cx_ref) let heap_total_root = mozjs::jsval::DoubleValue(0.0));
JS_DefineProperty(
cx,
obj_h,
c"heapTotal".as_ptr(),
heap_total_root.handle().into(),
JSPROP_ENUMERATE as u32,
);
rooted!(&in(cx_ref) let heap_used_root = mozjs::jsval::DoubleValue(0.0));
JS_DefineProperty(
cx,
obj_h,
c"heapUsed".as_ptr(),
heap_used_root.handle().into(),
JSPROP_ENUMERATE as u32,
);
rooted!(&in(cx_ref) let external_root = mozjs::jsval::DoubleValue(0.0));
JS_DefineProperty(
cx,
obj_h,
c"external".as_ptr(),
external_root.handle().into(),
JSPROP_ENUMERATE as u32,
);
rooted!(&in(cx_ref) let array_buffers_root = mozjs::jsval::DoubleValue(0.0));
JS_DefineProperty(
cx,
obj_h,
c"arrayBuffers".as_ptr(),
array_buffers_root.handle().into(),
JSPROP_ENUMERATE as u32,
);
args.rval().set(ObjectValue(obj.get()));
true
}
/// `process.memoryUsage.rss()` — Node.js surface where `memoryUsage` is a
/// callable function *and* has an `rss` sub-function property that returns
/// the current resident set size in bytes (number).
//
// @trace REQ-ENG-005 [api:process.memoryUsage.rss] — Bun mirrors Node.js:
// `process.memoryUsage.rss` is a function returning the live RSS in bytes.
// Upstream tests (buffer-from-encoding-leak.test.ts) call it directly to
// measure allocation growth across iterations.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn process_memory_usage_rss(
_cx: *mut JSContext,
_argc: u32,
vp: *mut JSVal,
) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let rss = current_process_rss_bytes();
args.rval().set(mozjs::jsval::DoubleValue(rss));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn process_kill(_cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
if _argc < 1 {
args.rval().set(BooleanValue(false));
return true;
}
let pid_val = args.get(0);
let pid = if pid_val.is_int32() {
pid_val.to_int32()
} else {
args.rval().set(BooleanValue(false));
return true;
};
let sig_num = if _argc >= 2 {
let sig_val = args.get(1);
if sig_val.is_int32() {
sig_val.to_int32()
} else {
15
}
} else {
15
};
// PORT NOTE(upstream c6461038): a crash signal sent to this process
// itself must die by the signal's default action, not enter the crash
// handler — POSIX delivers a self-sent signal before kill(2) returns, so
// the disposition has to change before the call. Upstream skips the reset
// when a JS listener owns the signal (`forwardSignal`); Bao installs no
// JS-listener disposition for the crash signals (spawn forwarding covers
// INT/TERM/HUP/QUIT only), so list membership + self-target is the whole
// predicate. kill(-1) never reaches the caller, hence `pid < -1` for the
// process-group case.
#[cfg(unix)]
{
let reaches_self = pid == libc::getpid()
|| pid == 0
|| (pid < -1 && pid == -(libc::getpgrp() as i32));
if reaches_self && bun_core::CRASH_HANDLER_SIGNALS.contains(&sig_num) {
let mut sa: libc::sigaction = unsafe { ::std::mem::zeroed() };
sa.sa_sigaction = libc::SIG_DFL;
unsafe {
libc::sigemptyset(&mut sa.sa_mask);
libc::sigaction(sig_num, &sa, core::ptr::null_mut());
}
}
}
#[cfg(windows)]
let _ = plat::kill(pid, sig_num);
#[cfg(not(windows))]
let _ = libc::kill(pid, sig_num);
args.rval().set(BooleanValue(true));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn process_umask(_cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
#[cfg(windows)]
let old = plat::umask(0o022);
#[cfg(not(windows))]
let old = unsafe { libc::umask(0o022) };
#[cfg(windows)]
plat::umask(old);
#[cfg(not(windows))]
unsafe { libc::umask(old) };
args.rval().set(Int32Value(old as i32));
true
}
thread_local! {
static PROCESS_START: RefCell<Option<::std::time::Instant>> = const { RefCell::new(None) };
}
pub fn init_process_start() {
PROCESS_START.with(|s| *s.borrow_mut() = Some(::std::time::Instant::now()));
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn set_env_fn(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc < 2 {
args.rval().set(UndefinedValue());
return true;
}
let key_val = *args.get(0).ptr;
let val_val = *args.get(1).ptr;
if !key_val.is_string() || !val_val.is_string() {
args.rval().set(UndefinedValue());
return true;
}
let key = crate::js_to_rust_string(cx, key_val);
let value = crate::js_to_rust_string(cx, val_val);
if !key.is_empty() && !key.contains('\0') && !value.contains('\0') {
::std::env::set_var(&key, &value);
}
args.rval().set(UndefinedValue());
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn del_env_fn(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc < 1 {
args.rval().set(UndefinedValue());
return true;
}
let key_val = *args.get(0).ptr;
if !key_val.is_string() {
args.rval().set(UndefinedValue());
return true;
}
let key = crate::js_to_rust_string(cx, key_val);
if !key.is_empty() && !key.contains('\0') {
::std::env::remove_var(&key);
}
args.rval().set(UndefinedValue());
true
}
/// Bun.exit(code) — set exit flag instead of calling std::process::exit().
/// The CLI main loop checks should_exit() and exits orderly,
/// allowing SmRuntimeGuard to drop (JS_DestroyContext + JS_ShutDown).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_exit(_cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let code = if argc > 0 && (*args.get(0).ptr).is_int32() {
(*args.get(0).ptr).to_int32()
} else {
0
};
crate::request_exit(code);
args.rval().set(UndefinedValue());
true
}
// ── per-platform process/fs primitives (cfg-blind face) ────────────────────
// Windows twins for the POSIX-only libc calls in this module. Signal-number
// consts that the windows CRT lacks carry their canonical POSIX values (the
// numbers are stable POSIX ABI); directed delivery maps to TerminateProcess
// (upstream node semantics on windows).
#[cfg(windows)]
mod plat {
use ::std::ffi::c_void;
pub const SIGHUP: i32 = 1;
pub const SIGQUIT: i32 = 3;
pub const SIGKILL: i32 = 9;
pub const SIGUSR1: i32 = 10;
pub const SIGUSR2: i32 = 12;
/// windows CRT `_O_NOINHERIT` — the O_CLOEXEC equivalent for CRT open().
pub const O_NOINHERIT: i32 = 0x0080;
#[link(name = "kernel32")]
unsafe extern "system" {
pub safe fn OpenProcess(dwdesiredaccess: u32, binherithandle: i32, dwprocessid: u32) -> *mut c_void;
pub safe fn TerminateProcess(hprocess: *mut c_void, uexitcode: u32) -> i32;
pub safe fn CloseHandle(hobject: *mut c_void) -> i32;
}
const PROCESS_TERMINATE: u32 = 0x0001;
pub fn kill(pid: i32, sig: i32) -> bool {
// No signal delivery on windows: every directed signal terminates.
let _ = sig;
if pid <= 0 {
return false;
}
// SAFETY: pid is caller-supplied numeric; the handle is opened and
// closed within this call.
let handle = unsafe { OpenProcess(PROCESS_TERMINATE, 0, pid as u32) };
if handle.is_null() {
return false;
}
let ok = unsafe { TerminateProcess(handle, 1) } != 0;
unsafe { CloseHandle(handle) };
ok
}
/// CRT `_pread` (UCRT) — the pread(2) equivalent.
///
/// BAO DELTA (win-cross, #18 hotfix): the pruned winsysroot UCRT libs
/// carry no `_pread` symbol (probe: `_read`/`_lseek`/`_write`/`_open`
/// whitelisted, `_pread` absent from both ucrt.lib and libucrt.lib), so
/// the offset read is spelled as `_lseek(fd, offset, SEEK_CUR)` +
/// `_read(fd, …)` — same bytes, same fd-position semantics.
pub fn pread(fd: i32, buf: *mut c_void, count: usize, offset: i64) -> isize {
#[link(name = "ucrt")]
unsafe extern "C" {
safe fn _lseek(fd: i32, offset: i64, origin: i32) -> i64;
safe fn _read(fd: i32, buf: *mut c_void, count: u32) -> i32;
}
const SEEK_SET: i32 = 0;
const SEEK_CUR: i32 = 1;
// TRUE pread semantics (the old body seeked offset relative to the
// CURRENT cursor via SEEK_CUR and left it advanced): absolute from 0
// AND the fd cursor is never consumed — save, absolute-seek, read,
// restore. Without the restore every whole-file read after the first
// saw EOF and returned empty (Bun.file(fd).text() idempotency FAIL:
// fdText2 empty; same shape as node_fs.rs readSync's pread face).
unsafe {
let saved = _lseek(fd, 0, SEEK_CUR);
if saved < 0 {
return -1;
}
if _lseek(fd, offset, SEEK_SET) < 0 {
return -1;
}
let n = _read(fd, buf, count as u32) as isize;
_lseek(fd, saved, SEEK_SET);
n
}
}
/// CRT `_umask` — mode masking differs (write bits only), matching the
/// upstream windows behavior.
pub fn umask(mode: u32) -> u32 {
#[link(name = "ucrt")]
unsafe extern "C" {
fn _umask(newmode: i32) -> i32;
}
(unsafe { _umask(mode as i32) }) as u32
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_sleep_sync(_cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc > 0 && (*args.get(0).ptr).is_number() {
let ms = (*args.get(0).ptr).to_number() as u64;
::std::thread::sleep(::std::time::Duration::from_millis(ms));
}
args.rval().set(UndefinedValue());
true
}
// @trace REQ-ENG-006 [api:Bun.nanoseconds] — nanoseconds elapsed since the
// process was STARTED (upstream: `bunVM().origin_timer.read()`).
//
// Audit found the old face returning a OnceLock-diff captured at FIRST CALL
// (~43ns), i.e. elapsed-since-first-JS-call, not since process start. The
// true process start is recovered from /proc/self/stat field 22 (starttime,
// clock ticks since boot) differenced against CLOCK_BOOTTIME — the kernel's
// own account of when this process began, with no initialization-order
// dependency.
static BAO_PROCESS_START_NS: ::std::sync::OnceLock<Option<u64>> = ::std::sync::OnceLock::new();
// Pairing contract: `process_start_ns_since_boot()` and `boottime_now_ns()`
// must share ONE time frame per platform — the caller's subtraction is only
// meaningful within a pair. unix: both arms are ns-since-boot (/proc starttime
// vs CLOCK_BOOTTIME). windows: both arms are ns-since-1601 (FILETIME × 100),
// so the subtraction yields true elapsed-since-process-creation.
#[cfg(windows)]
fn process_start_ns_since_boot() -> Option<u64> {
// GetProcessTimes's lpCreationTime is the kernel's own record of when
// this process was created — the windows twin of the /proc starttime
// read, with no first-call anchoring (the old windows arm returned None
// and degraded to the ~0-at-first-call fallback, failing the >1ms
// process-start baseline).
#[link(name = "kernel32")]
unsafe extern "system" {
// Plain unsafe decl matching node_os.rs's GetCurrentProcess signature —
// a `safe fn` re-decl would trip clashing_extern_declarations.
fn GetCurrentProcess() -> *mut ::std::ffi::c_void;
fn GetProcessTimes(
hprocess: *mut ::std::ffi::c_void,
lpcreationtime: *mut i64,
lpexittime: *mut i64,
lpkerneltime: *mut i64,
lpusertime: *mut i64,
) -> i32;
}
let (mut creation, mut exit, mut kernel, mut user) = (0i64, 0i64, 0i64, 0i64);
// SAFETY: GetCurrentProcess returns the pseudo-handle (-1), valid for
// GetProcessTimes; the out-params are local stack slots.
if unsafe {
GetProcessTimes(
GetCurrentProcess(),
&mut creation,
&mut exit,
&mut kernel,
&mut user,
)
} == 0
{
return None;
}
// FILETIME is 100ns ticks since 1601-01-01 (i64 bit-pattern fits u64 for
// any representable date); ×100 converts to ns in the same frame as
// boottime_now_ns below.
Some((creation as u64).saturating_mul(100))
}
#[cfg(unix)]
fn process_start_ns_since_boot() -> Option<u64> {
let stat = ::std::fs::read_to_string("/proc/self/stat").ok()?;
// Field 22 (starttime) sits after the last ')' (comm may contain spaces).
let after = stat.rsplit(')').next()?;
let fields: Vec<&str> = after.split_whitespace().collect();
// after-paren fields start at field 3 (state) → starttime is index 22-3.
let ticks: u64 = fields.get(19)?.parse().ok()?;
let hz = unsafe { libc::sysconf(libc::_SC_CLK_TCK) } as u64;
if hz == 0 {
return None;
}
Some(ticks * 1_000_000_000 / hz)
}
#[cfg(windows)]
fn boottime_now_ns() -> Option<u64> {
// Same FILETIME frame as process_start_ns_since_boot above; the precise
// variant (Win8+) keeps the subtraction free of 15.6ms timer quantum.
#[link(name = "kernel32")]
unsafe extern "system" {
safe fn GetSystemTimePreciseAsFileTime(lpsystemtimeasfiletime: *mut i64);
}
let mut now = 0i64;
// SAFETY: single out-param, local stack slot.
unsafe { GetSystemTimePreciseAsFileTime(&mut now) };
Some((now as u64).saturating_mul(100))
}
#[cfg(unix)]
fn boottime_now_ns() -> Option<u64> {
let mut ts = libc::timespec { tv_sec: 0, tv_nsec: 0 };
// CLOCK_BOOTTIME matches the /proc starttime reference frame.
if unsafe { libc::clock_gettime(libc::CLOCK_BOOTTIME, &mut ts) } != 0 {
return None;
}
Some(ts.tv_sec as u64 * 1_000_000_000 + ts.tv_nsec as u64)
}
unsafe extern "C" fn bun_nanoseconds(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let start = BAO_PROCESS_START_NS.get_or_init(process_start_ns_since_boot);
let elapsed_ns = match (start, boottime_now_ns()) {
(Some(s), Some(now)) => now.saturating_sub(*s),
// /proc or clock unavailable — monotonic fallback anchored at first
// call (explicit degradation path, Linux targets always take the
// real arm above).
_ => {
static FALLBACK_START: ::std::sync::OnceLock<::std::time::Instant> =
::std::sync::OnceLock::new();
FALLBACK_START
.get_or_init(::std::time::Instant::now)
.elapsed()
.as_nanos() as u64
}
};
// f64 carries the value; Number precision is sufficient for ~104 days of
// uptime at nanosecond scale (Number.MAX_SAFE_INTEGER ≈ 9e15 ns ≈ 104d).
let v = mozjs::jsval::DoubleValue(elapsed_ns as f64);
args.rval().set(v);
let _ = cx;
true
}
/// ISSUE #20: live env-permission consult for the process.env proxy traps —
/// the traps call this before every operation so `env_allowed=false`
/// fail-closes at ACCESS time (the guard may be installed after the proxy is
/// built: the permission install rides the first-evaluate callback drain).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bao_env_allowed_native(
cx: *mut JSContext,
_argc: u32,
vp: *mut JSVal,
) -> bool {
let allowed = crate::permission_bridge::check_env().is_ok();
let args = CallArgs::from_vp(vp, _argc);
args.rval().set(BooleanValue(allowed));
let _ = cx;
true
}
/// Bun.env getter — resolves `process.env` off the current global at access
/// time so both surfaces share one proxy (writes propagate to std::env).
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_env_getter(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let global = CurrentGlobalOrNull(cx);
if global.is_null() {
args.rval().set(UndefinedValue());
return true;
}
let mut wrapped = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped;
rooted!(&in(cx_ref) let global_root = global);
let mut process_v = UndefinedValue();
if !JS_GetProperty(
cx,
global_root.handle().into(),
c"process".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut process_v,
},
) || !process_v.is_object()
{
JS_ClearPendingException(cx);
args.rval().set(UndefinedValue());
return true;
}
rooted!(&in(cx_ref) let proc_obj = process_v.to_object());
let mut env_v = UndefinedValue();
if !JS_GetProperty(
cx,
proc_obj.handle().into(),
c"env".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut env_v,
},
) || env_v.is_null_or_undefined()
{
JS_ClearPendingException(cx);
args.rval().set(UndefinedValue());
return true;
}
args.rval().set(env_v);
true
}
#[allow(unsafe_op_in_unsafe_fn)]
/// `Bun.concatArrayBuffers(buffers, totalLength?, asUint8Array?)` — merge an
/// iterable of ArrayBuffer / TypedArray / DataView into a single buffer.
///
/// Matches Bun's signature and semantics:
/// - `buffers`: Array or iterable of buffer-like objects. Each element's
/// `byteLength` and `byteOffset` are honoured so views over larger
/// backing buffers transfer only their visible slice.
/// - `totalLength`: optional cap. If omitted, sum of all `byteLength`s. If
/// provided and larger than the sum, the tail is zero-filled. If smaller,
/// the concatenation is truncated.
/// - `asUint8Array`: when truthy, return a Uint8Array; otherwise an
/// ArrayBuffer (default).
///
/// @trace REQ-ENG-006 [algorithm:bun_concat_array_buffers] — TOCTOU-safe.
/// bun/test/js/node/buffer-concat.test.ts requires that, when a user-defined
/// getter detaches or resizes a previously-read buffer, the result is either:
/// - TypeError on detach (no memcpy from a freed pointer, no leaked heap), or
/// - the post-getter length on shrink.
///
/// We achieve this by (1) walking the list once via JS_GetElement to fire
/// every getter and snapshot each element's object identity, then (2)
/// re-reading each element's *current* (post-getter) data pointer + length
/// in a second sweep. A non-null length but null data pointer is the
/// detached-buffer fingerprint — we throw in that case.
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_concat_array_buffers(
cx: *mut JSContext,
argc: u32,
vp: *mut JSVal,
) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 || !(*args.get(0).ptr).is_object() {
args.rval().set(UndefinedValue());
return true;
}
let list_val = *args.get(0).ptr;
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let list_obj = list_val.to_object());
let list_h = list_obj.handle().into();
// Helper: probe whether `obj` is an ArrayBuffer (raw or wrapped).
unsafe fn is_array_buffer(obj: *mut JSObject) -> bool {
mozjs_sys::jsapi::JS::IsArrayBufferObjectMaybeShared(obj)
}
// Helper: get (length, data) of a raw ArrayBuffer (post-detach returns
// (0, null)).
unsafe fn ab_bytes(obj: *mut JSObject) -> (usize, *mut u8) {
let mut len: usize = 0;
let mut is_shared = false;
let mut data: *mut u8 = ::std::ptr::null_mut();
mozjs_sys::jsapi::JS::GetArrayBufferMaybeSharedLengthAndData(
obj,
&mut len,
&mut is_shared,
&mut data,
);
(len, data)
}
// Helper: get (length, data) of a typed-array view via the same path as
// `buffer_view_bytes` in globals.rs.
unsafe fn ta_bytes(obj: *mut JSObject) -> (usize, *mut u8) {
let mut length: usize = 0;
let mut is_shared = false;
let mut data: *mut u8 = ::std::ptr::null_mut();
let unwrapped =
mozjs_sys::jsapi::JS_GetObjectAsUint8Array(obj, &mut length, &mut is_shared, &mut data);
if unwrapped.is_null() {
(0, ::std::ptr::null_mut())
} else {
(length, data)
}
}
// First sweep: walk the list once, triggering all user-defined getters
// and snapshotting element object identities. We do not yet read any
// length / data — that's reserved for the second sweep so we observe
// post-getter state. Only Array-like inputs (`length` + indexed) are
// supported; that is the only shape used by upstream tests. Generic
// iterables would need a separate Symbol.iterator path.
let mut len_val = UndefinedValue();
JS_GetProperty(
cx,
list_h,
c"length".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut len_val,
},
);
let list_len: usize = if len_val.is_int32() {
len_val.to_int32().max(0) as usize
} else if len_val.is_double() {
let d = len_val.to_double();
if d.is_finite() && d > 0.0 {
d as usize
} else {
0
}
} else {
0
};
// GC rooting (SM-EVOLUTION #29 S2-续, frame-level dangling-nursery class —
// same family as the gc_store.rs rooting contract and the e4675351 vm
// registry fix): `JS_GetElement` can run a user-defined getter, which
// allocates and can trigger GC mid-loop. Elements held as bare
// `*mut JSObject` in a plain Vec are invisible to the tracer — earlier
// elements can be swept (getter-returned buffers with no other referent,
// e.g. `Bun.concatArrayBuffers({length: 2, get 0() {...}, get 1() {...}})`)
// or moved out of the nursery while we keep dereferencing them in the
// second sweep. RootedVec<Box<Heap<*mut JSObject>>> (servo structuredclone
// reader idiom) keeps every collected element tracer-visible
// (RootedTraceableSet) and barrier-pinned (Heap::boxed at a Box-stable
// address, so post-write-barrier slot addresses survive Vec growth). The
// rooted guard lives to end of frame, keeping backing stores alive through
// the memcpy pass.
let mut element_objs_rootable: RootableVec<Box<Heap<*mut JSObject>>> =
RootableVec::new_unrooted();
let mut element_objs = RootedVec::new(&mut element_objs_rootable);
for i in 0..list_len {
let mut elem = UndefinedValue();
JS_GetElement(
cx,
list_h,
i as u32,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut elem,
},
);
element_objs.push(Heap::boxed(if elem.is_object() {
elem.to_object()
} else {
::std::ptr::null_mut()
}));
}
// Second sweep: read each element's *current* (post-getter) length/data.
// Detect detach (length != 0 was expected, data null) and throw.
let mut element_lengths: Vec<usize> = Vec::with_capacity(list_len);
let mut element_data: Vec<*mut u8> = Vec::with_capacity(list_len);
let mut total: usize = 0;
for obj in element_objs.iter() {
if obj.get().is_null() {
element_lengths.push(0);
element_data.push(::std::ptr::null_mut());
continue;
}
let (len, data) = if unsafe { is_array_buffer(obj.get()) } {
unsafe { ab_bytes(obj.get()) }
} else {
unsafe { ta_bytes(obj.get()) }
};
// Detach fingerprint: data pointer is null even though the object is
// an ArrayBuffer / typed-array view. Bun throws to avoid UB and
// memory disclosure.
if data.is_null() {
let c_msg = c"Cannot perform Bun.concatArrayBuffers on a detached ArrayBuffer";
{
let mut cx_s = unsafe { mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx)) };
mozjs::error::throw_type_error(&mut cx_s, c_msg.as_ref());
}
return false;
}
element_lengths.push(len);
element_data.push(data);
total = total.saturating_add(len);
}
// Resolve target_total: explicit `totalLength` arg overrides the sum.
let mut target_total = total;
if argc > 1 {
let tl_val = *args.get(1).ptr;
if tl_val.is_int32() {
let v = tl_val.to_int32();
if v >= 0 {
target_total = v as usize;
}
} else if tl_val.is_double() {
let d = tl_val.to_double();
if d.is_finite() && d >= 0.0 {
target_total = d as usize;
}
} else if !tl_val.is_undefined() {
// Non-numeric, non-undefined totalLength: Bun treats Infinity
// (Number.POSITIVE_INFINITY) as "use the sum". Other junk falls
// back to the sum too.
if !(tl_val.is_double()
&& tl_val.to_double().is_infinite()
&& tl_val.to_double().is_sign_positive())
{
target_total = total;
}
}
}
// Allocate the output buffer. On 64 GiB test inputs (1024 × 64 MiB),
// try_reserve/Vec::with_capacity would succeed (virtual address space)
// but the actual byte write would touch unmapped pages and SIGSEGV.
// Pre-check the cap and throw an OOM-style RangeError so the test's
// toThrow(/Failed to allocate/i) matcher sees a clean exception.
const MAX_CONCAT_BYTES: usize = 4 * 1024 * 1024 * 1024 - 1; // SM typed-array ceiling
if target_total > MAX_CONCAT_BYTES {
let msg = format!(
"Failed to allocate ArrayBuffer of size {} bytes (exceeds {} byte limit)",
target_total, MAX_CONCAT_BYTES
);
let c_msg = ::std::ffi::CString::new(msg).unwrap_or_else(|_e| {
::std::ffi::CString::new("Failed to allocate ArrayBuffer").unwrap()
});
{
let mut cx_s = unsafe { mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx)) };
mozjs::error::throw_range_error(&mut cx_s, c_msg.as_ref());
}
return false;
}
let mut all_bytes: Vec<u8> = match target_total.checked_mul(1) {
Some(_) => Vec::new(),
None => {
let c_msg = c"Failed to allocate ArrayBuffer: size overflow";
{
let mut cx_s = unsafe { mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx)) };
mozjs::error::throw_range_error(&mut cx_s, c_msg.as_ref());
}
return false;
}
};
// try_reserve_exact would touch the OOM killer on huge sizes; we already
// capped above, so a plain resize is safe. Use resize so the buffer is
// zero-initialised — no uninitialized heap ever returned to JS.
if let Err(_) = all_bytes.try_reserve(target_total) {
let msg = format!(
"Failed to allocate ArrayBuffer of size {} bytes",
target_total
);
let c_msg = ::std::ffi::CString::new(msg).unwrap_or_else(|_e| {
::std::ffi::CString::new("Failed to allocate ArrayBuffer").unwrap()
});
{
let mut cx_s = unsafe { mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx)) };
mozjs::error::throw_range_error(&mut cx_s, c_msg.as_ref());
}
return false;
}
all_bytes.resize(target_total, 0u8);
// Copy pass: every (length, data) snapshot is current; no further JS
// runs between the second sweep and this loop, so the data pointers
// remain valid (no GC, no detach).
let mut cursor: usize = 0;
for (i, obj) in element_objs.iter().enumerate() {
if obj.get().is_null() || cursor >= target_total {
continue;
}
let len = *element_lengths.get(i).unwrap_or(&0);
if len == 0 {
continue;
}
let data = *element_data.get(i).unwrap_or(&::std::ptr::null_mut());
let copy_len = len.min(target_total.saturating_sub(cursor));
if copy_len > 0 && !data.is_null() {
::std::ptr::copy_nonoverlapping(data, all_bytes.as_mut_ptr().add(cursor), copy_len);
}
cursor = cursor.saturating_add(len);
}
// Build the output. asUint8=true → Uint8Array; otherwise ArrayBuffer.
let as_uint8 = argc > 2 && (*args.get(2).ptr).to_boolean();
let cx_ref = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
if as_uint8 {
let u8_obj = mozjs_sys::jsapi::JS_NewUint8Array(cx, all_bytes.len());
if u8_obj.is_null() {
args.rval().set(UndefinedValue());
return true;
}
rooted!(&in(cx_ref) let u8_root = u8_obj);
if !all_bytes.is_empty() {
let mut is_shared = false;
let data_ptr = mozjs_sys::jsapi::JS_GetUint8ArrayData(
u8_root.get(),
&mut is_shared,
::std::ptr::null(),
);
if !data_ptr.is_null() {
::std::ptr::copy_nonoverlapping(all_bytes.as_ptr(), data_ptr, all_bytes.len());
}
}
args.rval().set(mozjs::jsval::ObjectValue(u8_root.get()));
} else {
// Allocate an ArrayBuffer and copy bytes in.
let ab_obj = mozjs_sys::jsapi::JS::NewArrayBuffer(cx, all_bytes.len());
if ab_obj.is_null() {
args.rval().set(UndefinedValue());
return true;
}
rooted!(&in(cx_ref) let ab_root = ab_obj);
if !all_bytes.is_empty() {
let mut is_shared = false;
let data_ptr = mozjs_sys::jsapi::JS::GetArrayBufferMaybeSharedData(
ab_root.get(),
&mut is_shared,
::std::ptr::null(),
);
if !data_ptr.is_null() {
::std::ptr::copy_nonoverlapping(all_bytes.as_ptr(), data_ptr, all_bytes.len());
}
}
args.rval().set(mozjs::jsval::ObjectValue(ab_root.get()));
}
true
}
// ──────────────────────────────────────────────────────────────────────────
// @trace REQ-ENG-006 [api:Bun.CryptoHasher] — streaming hash constructor
//
// Bun.CryptoHasher(algorithm) creates a streaming hash object.
// Supported algorithms: sha256, sha512, sha1, md5.
// Methods: .update(data), .digest(encoding?)
// Encoding: "hex" (default), "base64", "buffer" (Uint8Array).
//
// Internal state is stored as a GcStore'd native pointer (boxed Vec<u8>
// accumulator + algorithm tag). This avoids SpiderMonkey GC pressure and
// keeps the hash state alive as long as the JS object references it.
// ──────────────────────────────────────────────────────────────────────────
/// Algorithm tag for CryptoHasher internal state.
#[derive(Clone, Copy, Debug)]
#[repr(u8)]
enum CryptoHasherAlgo {
Sha256 = 0,
Sha512 = 1,
Sha1 = 2,
Md5 = 3,
}
/// Internal state for a CryptoHasher instance.
struct CryptoHasherState {
algo: CryptoHasherAlgo,
data: Vec<u8>,
}
#[allow(dead_code)]
static CRYPTO_HASHER_CB_COUNTER: AtomicU64 = AtomicU64::new(0);
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_crypto_hasher_ctor(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let algo_str = if argc > 0 && (*args.get(0).ptr).is_string() {
crate::js_to_rust_string(cx, *args.get(0).ptr).to_ascii_lowercase()
} else {
"sha256".to_string()
};
let algo = match algo_str.as_str() {
"sha256" | "sha-256" => CryptoHasherAlgo::Sha256,
"sha512" | "sha-512" => CryptoHasherAlgo::Sha512,
"sha1" | "sha-1" => CryptoHasherAlgo::Sha1,
"md5" => CryptoHasherAlgo::Md5,
_ => {
let msg = format!("Unsupported algorithm: {}", algo_str);
let c_msg = ZBox::from_bytes(msg.as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
return false;
}
};
let state = Box::new(CryptoHasherState {
algo,
data: Vec::new(),
});
let state_ptr = Box::into_raw(state) as *mut ::std::ffi::c_void;
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj = JS_NewPlainObject(cx_ref));
if obj.get().is_null() {
args.rval().set(UndefinedValue());
return true;
}
// Store state pointer as PrivateValue
let priv_val = mozjs::jsval::PrivateValue(state_ptr);
rooted!(&in(cx_ref) let pv = priv_val);
JS_DefineProperty(
cx,
obj.handle().into(),
c"_statePtr".as_ptr(),
pv.handle().into(),
0,
);
// Store algorithm tag
let algo_val = Int32Value(algo as i32);
rooted!(&in(cx_ref) let av = algo_val);
JS_DefineProperty(
cx,
obj.handle().into(),
c"_algo".as_ptr(),
av.handle().into(),
0,
);
JS_DefineFunction(
cx_ref,
obj.handle(),
c"update".as_ptr(),
Some(crypto_hasher_update),
1,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx_ref,
obj.handle(),
c"digest".as_ptr(),
Some(crypto_hasher_digest),
0,
JSPROP_ENUMERATE as u32,
);
args.rval().set(ObjectValue(obj.get()));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn crypto_hasher_update(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 {
// No data to update — return this for chaining
args.rval().set(*args.thisv().ptr);
return true;
}
let this = args.thisv();
if !this.is_object() {
args.rval().set(UndefinedValue());
return true;
}
// Read state pointer
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let this_obj = this.to_object());
let mut state_val = UndefinedValue();
let _ = bao_stealth::engine_props::get_property_clearing(
cx,
this_obj.handle().into(),
c"_statePtr",
&mut state_val,
);
if !state_val.is_double() || (state_val.asBits_ & 0xFFFF000000000000) != 0 {
args.rval().set(*args.thisv().ptr);
return true;
}
let state_ptr = state_val.to_private() as *mut CryptoHasherState;
if state_ptr.is_null() {
args.rval().set(*args.thisv().ptr);
return true;
}
// Extract input data
let input = *args.get(0).ptr;
let data = if input.is_string() {
crate::js_to_rust_string(cx, input).into_bytes()
} else if input.is_object() {
rooted!(&in(cx_ref) let arr_obj = input.to_object());
let mut len_val = UndefinedValue();
JS_GetProperty(
cx,
arr_obj.handle().into(),
c"length".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut len_val,
},
);
let len = if len_val.is_int32() {
len_val.to_int32().max(0) as u32
} else {
0
};
let mut bytes = Vec::with_capacity(len as usize);
for i in 0..len {
let mut byte_val = Int32Value(0);
JS_GetElement(
cx,
arr_obj.handle().into(),
i,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut byte_val,
},
);
bytes.push(if byte_val.is_int32() {
byte_val.to_int32() as u8
} else {
0
});
}
bytes
} else {
Vec::new()
};
// Append to accumulator
(*state_ptr).data.extend_from_slice(&data);
// Return this for chaining
args.rval().set(ObjectValue(this_obj.get()));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn crypto_hasher_digest(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let this = args.thisv();
if !this.is_object() {
args.rval().set(UndefinedValue());
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 this_obj = this.to_object());
// Read state pointer
let mut state_val = UndefinedValue();
let _ = bao_stealth::engine_props::get_property_clearing(
cx,
this_obj.handle().into(),
c"_statePtr",
&mut state_val,
);
if !state_val.is_double() || (state_val.asBits_ & 0xFFFF000000000000) != 0 {
args.rval().set(UndefinedValue());
return true;
}
let state_ptr = state_val.to_private() as *mut CryptoHasherState;
if state_ptr.is_null() {
args.rval().set(UndefinedValue());
return true;
}
let encoding = if argc > 0 && (*args.get(0).ptr).is_string() {
crate::js_to_rust_string(cx, *args.get(0).ptr).to_ascii_lowercase()
} else {
"hex".to_string()
};
let state = &*state_ptr;
let hash_bytes = match state.algo {
CryptoHasherAlgo::Sha256 => {
let mut hasher = bun_sha_hmac::SHA256::init();
hasher.update(&state.data);
let mut out = [0u8; bun_sha_hmac::SHA256::DIGEST];
hasher.r#final(&mut out);
out.to_vec()
}
CryptoHasherAlgo::Sha512 => {
let mut hasher = bun_sha_hmac::SHA512::init();
hasher.update(&state.data);
let mut out = [0u8; bun_sha_hmac::SHA512::DIGEST];
hasher.r#final(&mut out);
out.to_vec()
}
CryptoHasherAlgo::Sha1 => {
let mut hasher = bun_sha_hmac::SHA1::init();
hasher.update(&state.data);
let mut out = [0u8; bun_sha_hmac::SHA1::DIGEST];
hasher.r#final(&mut out);
out.to_vec()
}
CryptoHasherAlgo::Md5 => {
let mut hasher = bun_sha_hmac::MD5::init();
hasher.update(&state.data);
let mut out = [0u8; bun_sha_hmac::MD5::DIGEST];
hasher.r#final(&mut out);
out.to_vec()
}
};
// @trace REQ-ENG-006 [api:Bun.CryptoHasher.digest] — digest() with NO
// encoding argument returns a Buffer of the raw digest bytes (upstream
// Bun / node parity: node_crypto::hash_digest's no-arg branch). The old
// no-arg default of "hex" broke Buffer.isBuffer() and returned 64 hex
// chars instead of 32 raw bytes for sha256. Only an EXPLICIT string
// argument selects an encoded form; a present-but-non-string argument
// keeps the historical "hex" fallback.
if argc == 0 {
let buf_obj = crate::globals::create_buffer_object(cx, &hash_bytes);
if buf_obj.is_null() {
args.rval().set(UndefinedValue());
return true;
}
args.rval().set(mozjs::jsval::ObjectValue(buf_obj));
return true;
}
match encoding.as_str() {
"buffer" => {
// Return Uint8Array
let u8_obj = mozjs_sys::jsapi::JS_NewUint8Array(cx, hash_bytes.len());
if u8_obj.is_null() {
args.rval().set(UndefinedValue());
return true;
}
rooted!(&in(cx_ref) let u8_root = u8_obj);
if !hash_bytes.is_empty() {
let mut is_shared = false;
let data_ptr = mozjs_sys::jsapi::JS_GetUint8ArrayData(
u8_root.get(),
&mut is_shared,
::std::ptr::null(),
);
if !data_ptr.is_null() {
::std::ptr::copy_nonoverlapping(
hash_bytes.as_ptr(),
data_ptr,
hash_bytes.len(),
);
}
}
args.rval().set(ObjectValue(u8_root.get()));
}
"base64" => {
// Base64 encoding via bun_base64 (workspace crate)
let b64_bytes = bun_base64::encode_alloc(&hash_bytes);
let b64_str = String::from_utf8_lossy(&b64_bytes).into_owned();
let c_b64 = ZBox::from_bytes(b64_str.as_bytes());
let js_str = JS_NewStringCopyZ(cx, c_b64.as_ptr());
args.rval().set(if js_str.is_null() {
UndefinedValue()
} else {
StringValue(&*js_str)
});
}
_ => {
// "hex" (default)
let hex: String = bun_core::fmt::bytes_to_hex_lower_string(&hash_bytes);
let c_hex = ZBox::from_bytes(hex.as_bytes());
let js_str = JS_NewStringCopyZ(cx, c_hex.as_ptr());
args.rval().set(if js_str.is_null() {
UndefinedValue()
} else {
StringValue(&*js_str)
});
}
}
true
}
// ──────────────────────────────────────────────────────────────────────────
// @trace REQ-ENG-006 [api:Bun.gzip/deflate/inflate/gunzip] — compression
// ──────────────────────────────────────────────────────────────────────────
/// Extract byte data from a JS value (string, ArrayBuffer, TypedArray, or ArrayBufferView).
/// Returns `None` for unrecognized objects (non-ArrayBuffer/TypedArray/ArrayBufferView).
pub(crate) unsafe fn extract_bytes_from_jsval(cx: *mut JSContext, val: JSVal) -> Option<Vec<u8>> {
if val.is_string() {
Some(crate::js_to_rust_string(cx, val).into_bytes())
} else if val.is_object() {
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj = val.to_object());
// Try ArrayBuffer
if mozjs_sys::jsapi::JS::IsArrayBufferObjectMaybeShared(obj.get()) {
let mut len: usize = 0;
let mut is_shared = false;
let mut data: *mut u8 = ::std::ptr::null_mut();
// SAFETY: obj is a valid ArrayBuffer object; out-params are stack locals.
mozjs_sys::jsapi::JS::GetArrayBufferMaybeSharedLengthAndData(
obj.get(),
&mut len,
&mut is_shared,
&mut data,
);
if data.is_null() {
return Some(Vec::new());
}
let mut bytes = vec![0u8; len];
// SAFETY: data points to len bytes within the ArrayBuffer's owned memory.
::std::ptr::copy_nonoverlapping(data, bytes.as_mut_ptr(), len);
return Some(bytes);
}
// Try Uint8Array (also matches Buffer, which is a Uint8Array subclass)
let mut length: usize = 0;
let mut is_shared = false;
let mut data: *mut u8 = ::std::ptr::null_mut();
// SAFETY: obj is a valid JS object; out-params are stack locals.
let unwrapped = mozjs_sys::jsapi::JS_GetObjectAsUint8Array(
obj.get(),
&mut length,
&mut is_shared,
&mut data,
);
if !unwrapped.is_null() && !data.is_null() {
let mut bytes = vec![0u8; length];
// SAFETY: data points to length bytes within the TypedArray's buffer.
::std::ptr::copy_nonoverlapping(data, bytes.as_mut_ptr(), length);
return Some(bytes);
}
if !unwrapped.is_null() {
return Some(Vec::new());
}
// Try ArrayBufferView (DataView, Int8Array, Int16Array, Float32Array, etc.)
let mut view_length: usize = 0;
let mut view_shared = false;
let mut view_data: *mut u8 = ::std::ptr::null_mut();
// SAFETY: obj is a valid JS object; out-params are stack locals.
let view_unwrapped = mozjs_sys::jsapi::JS_GetObjectAsArrayBufferView(
obj.get(),
&mut view_length,
&mut view_shared,
&mut view_data,
);
if !view_unwrapped.is_null() && !view_data.is_null() {
let mut bytes = vec![0u8; view_length];
// SAFETY: view_data points to view_length bytes within the view's buffer.
::std::ptr::copy_nonoverlapping(view_data, bytes.as_mut_ptr(), view_length);
return Some(bytes);
}
if !view_unwrapped.is_null() {
return Some(Vec::new());
}
None
} else {
None
}
}
/// Create a JS Uint8Array from a byte slice.
pub(crate) unsafe fn bytes_to_js_uint8array(cx: *mut JSContext, bytes: &[u8]) -> JSVal {
let u8_obj = mozjs_sys::jsapi::JS_NewUint8Array(cx, bytes.len());
if u8_obj.is_null() {
return UndefinedValue();
}
if !bytes.is_empty() {
let mut is_shared = false;
let data_ptr =
mozjs_sys::jsapi::JS_GetUint8ArrayData(u8_obj, &mut is_shared, ::std::ptr::null());
if !data_ptr.is_null() {
::std::ptr::copy_nonoverlapping(bytes.as_ptr(), data_ptr, bytes.len());
}
}
ObjectValue(u8_obj)
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_gzip(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 {
JS_ReportErrorUTF8(cx, c"Bun.gzip() requires data".as_ptr());
return false;
}
let input = match extract_bytes_from_jsval(cx, *args.get(0).ptr) {
Some(d) => d,
None => {
JS_ReportErrorUTF8(cx, c"Bun.gzip() requires string or ArrayBuffer".as_ptr());
return false;
}
};
use flate2::Compression;
use flate2::write::GzEncoder;
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
::std::io::Write::write_all(&mut encoder, &input).ok();
match encoder.finish() {
Ok(compressed) => {
args.rval().set(bytes_to_js_uint8array(cx, &compressed));
true
}
Err(e) => {
let msg = format!("Bun.gzip() failed: {}", e);
let c_msg = ZBox::from_bytes(msg.as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
false
}
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_deflate(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 {
JS_ReportErrorUTF8(cx, c"Bun.deflate() requires data".as_ptr());
return false;
}
let input = match extract_bytes_from_jsval(cx, *args.get(0).ptr) {
Some(d) => d,
None => {
JS_ReportErrorUTF8(cx, c"Bun.deflate() requires string or ArrayBuffer".as_ptr());
return false;
}
};
use flate2::Compression;
use flate2::write::DeflateEncoder;
let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
::std::io::Write::write_all(&mut encoder, &input).ok();
match encoder.finish() {
Ok(compressed) => {
args.rval().set(bytes_to_js_uint8array(cx, &compressed));
true
}
Err(e) => {
let msg = format!("Bun.deflate() failed: {}", e);
let c_msg = ZBox::from_bytes(msg.as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
false
}
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_inflate(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 {
JS_ReportErrorUTF8(cx, c"Bun.inflate() requires data".as_ptr());
return false;
}
let input = match extract_bytes_from_jsval(cx, *args.get(0).ptr) {
Some(d) => d,
None => {
JS_ReportErrorUTF8(cx, c"Bun.inflate() requires string or ArrayBuffer".as_ptr());
return false;
}
};
use flate2::write::DeflateDecoder;
let mut decoder = DeflateDecoder::new(Vec::new());
::std::io::Write::write_all(&mut decoder, &input).ok();
match decoder.finish() {
Ok(decompressed) => {
args.rval().set(bytes_to_js_uint8array(cx, &decompressed));
true
}
Err(e) => {
let msg = format!("Bun.inflate() failed: {}", e);
let c_msg = ZBox::from_bytes(msg.as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
false
}
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_gunzip(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 {
JS_ReportErrorUTF8(cx, c"Bun.gunzip() requires data".as_ptr());
return false;
}
let input = match extract_bytes_from_jsval(cx, *args.get(0).ptr) {
Some(d) => d,
None => {
JS_ReportErrorUTF8(cx, c"Bun.gunzip() requires string or ArrayBuffer".as_ptr());
return false;
}
};
use flate2::write::GzDecoder;
let mut decoder = GzDecoder::new(Vec::new());
::std::io::Write::write_all(&mut decoder, &input).ok();
match decoder.finish() {
Ok(decompressed) => {
args.rval().set(bytes_to_js_uint8array(cx, &decompressed));
true
}
Err(e) => {
let msg = format!("Bun.gunzip() failed: {}", e);
let c_msg = ZBox::from_bytes(msg.as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
false
}
}
}
// ──────────────────────────────────────────────────────────────────────────
// @trace REQ-ENG-006 [api:Bun.fileURLToPath/pathToFileURL] — URL<->path
// ──────────────────────────────────────────────────────────────────────────
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_file_url_to_path(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 {
JS_ReportErrorUTF8(cx, c"Bun.fileURLToPath() requires a URL".as_ptr());
return false;
}
let url_val = *args.get(0).ptr;
let url_str = if url_val.is_string() {
crate::js_to_rust_string(cx, url_val)
} else {
JS_ReportErrorUTF8(cx, c"Bun.fileURLToPath() requires a string".as_ptr());
return false;
};
// Use bun_url's WHATWG URL parser to extract path from file:// URL
let path = if url_str.starts_with("file://") {
let bun_str = bun_core::String::borrow_utf8(url_str.as_bytes());
let result = bun_url::whatwg::path_from_file_url(&bun_str);
if result.tag() == bun_core::Tag::Dead {
url_str.clone()
} else {
let utf8 = result.to_utf8();
let s = ::std::str::from_utf8(utf8.slice())
.unwrap_or(&url_str)
.to_string();
// bc713f9543: owning `result` — `Drop` replaces the manual deref.
s
}
} else {
// Not a file:// URL — return as-is (Bun behavior)
url_str.clone()
};
// Decode percent-encoding in the path
let decoded = percent_decode_path(&path);
// Non-ASCII decoded paths must reach JS as code points (same
// Latin-1 mojibake class as the Bun.file path property).
let js_str = js_string_from_utf8(cx, &decoded);
args.rval().set(if js_str.is_null() {
UndefinedValue()
} else {
StringValue(&*js_str)
});
true
}
/// Minimal percent-decode for file paths (decode %XX sequences).
fn percent_decode_path(s: &str) -> String {
let mut result = Vec::with_capacity(s.len());
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len() {
let hi = hex_digit(bytes[i + 1]);
let lo = hex_digit(bytes[i + 2]);
if hi.is_some() && lo.is_some() {
result.push(hi.unwrap() << 4 | lo.unwrap());
i += 3;
continue;
}
}
result.push(bytes[i]);
i += 1;
}
String::from_utf8_lossy(&result).into_owned()
}
fn hex_digit(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_path_to_file_url(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 {
JS_ReportErrorUTF8(cx, c"Bun.pathToFileURL() requires a path".as_ptr());
return false;
}
let path_val = *args.get(0).ptr;
let path_str = if path_val.is_string() {
crate::js_to_rust_string(cx, path_val)
} else {
JS_ReportErrorUTF8(cx, c"Bun.pathToFileURL() requires a string".as_ptr());
return false;
};
// Use bun_url's WHATWG URL parser to convert path to file:// URL
let url = {
let bun_str = bun_core::String::borrow_utf8(path_str.as_bytes());
let result = bun_url::whatwg::file_url_from_string(&bun_str);
if result.tag() == bun_core::Tag::Dead {
// Fallback: construct file:// URL manually
let canonical = ::std::path::Path::new(&path_str)
.canonicalize()
.unwrap_or_else(|_| ::std::path::PathBuf::from(&path_str));
format!("file://{}", canonical.to_string_lossy())
} else {
let utf8 = result.to_utf8();
let s = ::std::str::from_utf8(utf8.slice())
.unwrap_or("")
.to_string();
// bc713f9543: owning `result` — `Drop` replaces the manual deref.
s
}
};
let c_url = ZBox::from_bytes(url.as_bytes());
let js_str = JS_NewStringCopyZ(cx, c_url.as_ptr());
args.rval().set(if js_str.is_null() {
UndefinedValue()
} else {
StringValue(&*js_str)
});
true
}
// ──────────────────────────────────────────────────────────────────────────
// @trace REQ-ENG-006 [api:Bun.semver] — semver parsing (JS IIFE)
// ──────────────────────────────────────────────────────────────────────────
unsafe fn install_bun_semver(
cx: &mut mozjs::context::JSContext,
bun_obj: mozjs::rust::Handle<*mut JSObject>,
) {
let src = r#"(function() {
var semver = {};
// Minimal semver.satisfies(version, range) — supports exact match,
// ^x.y.z (caret), ~x.y.z (tilde), >x.y.z, >=x.y.z, <x.y.z, <=x.y.z,
// and x.y.z - a.b.c ranges.
function parseSemver(v) {
var m = String(v).trim().match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
if (!m) return null;
// Split trailing build metadata (`+…`) off the prerelease tail so
// order() can apply SemVer 2.0 precedence (build is ignored).
var rest = m[4];
var plus = rest.indexOf('+');
var pre = plus === -1 ? rest : rest.slice(0, plus);
return {
major: +m[1],
minor: +m[2],
patch: +m[3],
prerelease: pre,
};
}
function cmpSemver(a, b) {
if (a.major !== b.major) return a.major - b.major;
if (a.minor !== b.minor) return a.minor - b.minor;
return a.patch - b.patch;
}
// `order` is a NATIVE function defined after evaluation (see
// install_bun_semver): Bun's own bun_semver crate, mirroring upstream
// SemverObject.order (parse → max() → orderWithoutBuild).
semver.satisfies = function satisfies(version, range) {
var v = parseSemver(version);
if (!v) return false;
range = String(range).trim();
// Caret range: ^1.2.3 → >=1.2.3 <2.0.0
if (range[0] === '^') {
var r = parseSemver(range.slice(1));
if (!r) return false;
return cmpSemver(v, r) >= 0 && v.major === r.major;
}
// Tilde range: ~1.2.3 → >=1.2.3 <1.3.0
if (range[0] === '~') {
var r = parseSemver(range.slice(1));
if (!r) return false;
return cmpSemver(v, r) >= 0 && v.major === r.major && v.minor === r.minor;
}
// Comparison operators
if (range.slice(0, 2) === '>=') {
var r = parseSemver(range.slice(2));
return r ? cmpSemver(v, r) >= 0 : false;
}
if (range[0] === '>') {
var r = parseSemver(range.slice(1));
return r ? cmpSemver(v, r) > 0 : false;
}
if (range.slice(0, 2) === '<=') {
var r = parseSemver(range.slice(2));
return r ? cmpSemver(v, r) <= 0 : false;
}
if (range[0] === '<') {
var r = parseSemver(range.slice(1));
return r ? cmpSemver(v, r) < 0 : false;
}
// Exact match
var r = parseSemver(range);
return r ? cmpSemver(v, r) === 0 : false;
};
semver.parse = parseSemver;
return semver;
})()"#;
let mut text = mozjs::rust::transform_str_to_source_text(src);
let opts = mozjs::glue::NewCompileOptions(cx.raw_cx(), c"<bun:semver>".as_ptr(), 1);
if opts.is_null() {
return;
}
let mut rval = UndefinedValue();
let rval_h = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut rval,
};
let ok = mozjs_sys::jsapi::JS::Evaluate2(cx.raw_cx(), opts, &mut text, rval_h);
mozjs::glue::DeleteCompileOptions(opts);
if ok && rval.is_object() {
rooted!(&in(cx) let semver_obj = rval.to_object());
// `order` — native, backed by the bun_semver crate (Bun's own
// engine; the same parser install/resolver use). Mirrors upstream
// SemverObject.order: parse → max() → orderWithoutBuild.
JS_DefineFunction(
cx,
semver_obj.handle(),
c"order".as_ptr(),
Some(bun_semver_order),
2,
JSPROP_ENUMERATE as u32,
);
JS_DefineProperty3(
cx,
bun_obj,
c"semver".as_ptr(),
semver_obj.handle(),
JSPROP_ENUMERATE as u32,
);
}
}
/// `Bun.semver.order(a, b)` → -1 | 0 | 1.
///
/// Mirrors upstream `semver_jsc/SemverObject.zig order`: two string
/// arguments (else throw "Expected two arguments"); non-ASCII input → 0;
/// each side parsed by `bun_semver::Version::parse_utf8` (invalid → throw
/// "Invalid SemVer: {s}"); partial versions canonicalize via `.max()`;
/// comparison via `order_without_build` (build metadata ignored, SemVer
/// 2.0).
///
/// @trace REQ-ENG-006 [api:Bun.semver.order] [reuse:bun_semver]
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_semver_order(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
use bun_semver::version::VersionInt;
use ::std::cmp::Ordering;
let args = CallArgs::from_vp(vp, argc);
if argc < 2 {
JS_ReportErrorUTF8(cx, c"Expected two arguments".as_ptr());
return false;
}
let a_val = *args.get(0).ptr;
let b_val = *args.get(1).ptr;
if !a_val.is_string() || !b_val.is_string() {
JS_ReportErrorUTF8(cx, c"Expected two arguments".as_ptr());
return false;
}
let a_str = crate::js_to_rust_string(cx, a_val);
let b_str = crate::js_to_rust_string(cx, b_val);
// Upstream: non-ASCII input short-circuits to 0 (semver is ASCII-only).
if !a_str.is_ascii() || !b_str.is_ascii() {
args.rval().set(Int32Value(0));
return true;
}
let a_bytes = a_str.as_bytes().to_vec();
let b_bytes = b_str.as_bytes().to_vec();
let a_result = bun_semver::VersionType::<u32>::parse_utf8(&a_bytes);
let b_result = bun_semver::VersionType::<u32>::parse_utf8(&b_bytes);
if !a_result.valid {
let msg = format!("Invalid SemVer: {}", a_str);
let c_msg = ZBox::from_bytes(msg.as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
return false;
}
if !b_result.valid {
let msg = format!("Invalid SemVer: {}", b_str);
let c_msg = ZBox::from_bytes(msg.as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
return false;
}
let a_version = a_result.version.max();
let b_version = b_result.version.max();
let out = match a_version.order_without_build(b_version, &a_bytes, &b_bytes) {
Ordering::Less => -1,
Ordering::Equal => 0,
Ordering::Greater => 1,
};
args.rval().set(Int32Value(out));
true
}
// ──────────────────────────────────────────────────────────────────────────
// @trace REQ-ENG-006 [api:Bun.escapeHTML] — HTML entity escaping
// ──────────────────────────────────────────────────────────────────────────
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_escape_html(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 {
let js_str = JS_NewStringCopyZ(cx, c"".as_ptr());
args.rval().set(if js_str.is_null() {
UndefinedValue()
} else {
StringValue(&*js_str)
});
return true;
}
let val = *args.get(0).ptr;
if !val.is_string() {
args.rval().set(UndefinedValue());
return true;
}
let s = crate::js_to_rust_string(cx, val);
let mut out = String::with_capacity(s.len() * 2);
for ch in s.chars() {
match ch {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(ch),
}
}
let js_str = js_string_from_utf8(cx, &out);
args.rval().set(if js_str.is_null() {
UndefinedValue()
} else {
StringValue(&*js_str)
});
true
}
// ──────────────────────────────────────────────────────────────────────────
// @trace REQ-ENG-006 [api:Bun.Mime] — MIME type utility class (JS IIFE)
// ──────────────────────────────────────────────────────────────────────────
// ──────────────────────────────────────────────────────────────────────────
// @trace REQ-ENG-006 [api:Bun.stdin/stdout/stderr] — Bun.file(fd) wrappers
// ──────────────────────────────────────────────────────────────────────────
/// Create a JS object representing Bun.file(fd) for the given file descriptor.
unsafe fn make_bun_file_for_fd(cx: &mut mozjs::context::JSContext, fd: i32) -> *mut JSObject {
rooted!(&in(cx) let file_obj = JS_NewPlainObject(cx));
if file_obj.get().is_null() {
return ::std::ptr::null_mut();
}
let fd_val = Int32Value(fd);
rooted!(&in(cx) let fv = fd_val);
JS_DefineProperty(
cx.raw_cx(),
file_obj.handle().into(),
c"fd".as_ptr(),
fv.handle().into(),
JSPROP_ENUMERATE as u32,
);
// Add a path property for the fd. Non-std fds use their /proc/self/fd/N
// link so the shared exists() face stats the real descriptor.
let proc_path;
let path_str = match fd {
0 => "/dev/stdin",
1 => "/dev/stdout",
2 => "/dev/stderr",
_ => {
proc_path = format!("/proc/self/fd/{}", fd);
proc_path.as_str()
}
};
let c_path = ZBox::from_bytes(path_str.as_bytes());
let js_path = JS_NewStringCopyZ(cx.raw_cx(), c_path.as_ptr());
if !js_path.is_null() {
rooted!(&in(cx) let pv = StringValue(&*js_path));
JS_DefineProperty(
cx.raw_cx(),
file_obj.handle().into(),
c"path".as_ptr(),
pv.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
// Add readable/writable properties
let (readable, writable) = match fd {
0 => (true, false),
1 | 2 => (false, true),
_ => (true, true),
};
rooted!(&in(cx) let rv = BooleanValue(readable));
JS_DefineProperty(
cx.raw_cx(),
file_obj.handle().into(),
c"readable".as_ptr(),
rv.handle().into(),
JSPROP_ENUMERATE as u32,
);
rooted!(&in(cx) let wv = BooleanValue(writable));
JS_DefineProperty(
cx.raw_cx(),
file_obj.handle().into(),
c"writable".as_ptr(),
wv.handle().into(),
JSPROP_ENUMERATE as u32,
);
// Same method face as the path form: exists()/text()/json()/
// arrayBuffer()/slice() — the read methods go straight to the fd (the
// /proc path is only a display identity).
// @trace REQ-ENG-006 [api:Bun.file(fd) method face]
define_bunfile_methods(cx, file_obj.handle());
file_obj.get()
}
// ──────────────────────────────────────────────────────────────────────────
// @trace REQ-ENG-006 [api:Bun.deepLink] — throws "not implemented"
// ──────────────────────────────────────────────────────────────────────────
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_deep_link(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
JS_ReportErrorUTF8(
cx,
c"Bun.deepLink() is not implemented in this environment".as_ptr(),
);
args.rval().set(UndefinedValue());
false
}
// ──────────────────────────────────────────────────────────────────────────
// @trace REQ-ENG-006 [api:Bun.openInNewTab] — open URL in new tab
// ──────────────────────────────────────────────────────────────────────────
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bun_open_in_new_tab(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 {
JS_ReportErrorUTF8(cx, c"Bun.openInNewTab() requires a URL".as_ptr());
return false;
}
let url_val = *args.get(0).ptr;
if !url_val.is_string() {
JS_ReportErrorUTF8(cx, c"Bun.openInNewTab() requires a string URL".as_ptr());
return false;
}
let url = crate::js_to_rust_string(cx, url_val);
// Try to open via xdg-open (Linux) or open (macOS)
#[cfg(target_family = "unix")]
{
let opener = if cfg!(target_os = "macos") {
"open"
} else {
"xdg-open"
};
let _ = ::std::process::Command::new(opener).arg(&url).spawn();
}
// Return undefined (Bun behavior)
args.rval().set(UndefinedValue());
true
}
// @trace REQ-ENG-006 [req:REQ-ENG-006] [level:unit]
#[cfg(test)]
mod tests {
use super::*;
use bun_sha_hmac;
// ── TestCase ──
#[test]
fn test_case_stores_name() {
let tc = TestCase {
name: "my test".to_string(),
callback_key: "test_cb_0".to_string(),
};
assert_eq!(tc.name, "my test");
}
#[test]
fn test_case_callback_key_default() {
let tc = TestCase {
name: String::new(),
callback_key: "test_cb_1".to_string(),
};
assert!(!tc.callback_key.is_empty());
}
// ── BunServeUserData ──
#[test]
fn bun_serve_user_data_default_fields() {
let data = BunServeUserData {
fetch_cb_key: None,
websocket_cb_key: None,
app_ptr: ::std::ptr::null_mut(),
hostname: "localhost".to_string(),
port: 3000,
actual_port: AtomicU16::new(0),
cx: ::std::ptr::null_mut(),
};
assert!(data.fetch_cb_key.is_none());
assert!(data.websocket_cb_key.is_none());
assert!(data.app_ptr.is_null());
assert_eq!(data.hostname, "localhost");
assert_eq!(data.port, 3000);
}
#[test]
fn bun_serve_user_data_with_fetch_cb() {
let data = BunServeUserData {
fetch_cb_key: Some("serve_fetch_0".to_string()),
websocket_cb_key: None,
app_ptr: ::std::ptr::null_mut(),
hostname: "0.0.0.0".to_string(),
port: 8080,
actual_port: AtomicU16::new(0),
cx: ::std::ptr::null_mut(),
};
assert!(data.fetch_cb_key.is_some());
assert!(data.websocket_cb_key.is_none());
assert_eq!(data.port, 8080);
}
#[test]
fn bun_serve_user_data_with_websocket_cb() {
let data = BunServeUserData {
fetch_cb_key: None,
websocket_cb_key: Some("serve_ws_0".to_string()),
app_ptr: ::std::ptr::null_mut(),
hostname: "0.0.0.0".to_string(),
port: 8080,
actual_port: AtomicU16::new(0),
cx: ::std::ptr::null_mut(),
};
assert!(data.fetch_cb_key.is_none());
assert!(data.websocket_cb_key.is_some());
}
#[test]
fn bun_serve_user_data_hostname_variants() {
for host in &["localhost", "0.0.0.0", "127.0.0.1", "::"] {
let data = BunServeUserData {
fetch_cb_key: None,
websocket_cb_key: None,
app_ptr: ::std::ptr::null_mut(),
hostname: host.to_string(),
port: 80,
actual_port: AtomicU16::new(0),
cx: ::std::ptr::null_mut(),
};
assert_eq!(data.hostname, *host);
}
}
#[test]
fn bun_serve_user_data_port_boundaries() {
let data = BunServeUserData {
fetch_cb_key: None,
websocket_cb_key: None,
app_ptr: ::std::ptr::null_mut(),
hostname: String::new(),
port: 0,
actual_port: AtomicU16::new(0),
cx: ::std::ptr::null_mut(),
};
assert_eq!(data.port, 0);
let data = BunServeUserData {
fetch_cb_key: None,
websocket_cb_key: None,
app_ptr: ::std::ptr::null_mut(),
hostname: String::new(),
port: 65535,
actual_port: AtomicU16::new(0),
cx: ::std::ptr::null_mut(),
};
assert_eq!(data.port, 65535);
}
// @trace BCE-20260618-005 [level:regression] [req:REQ-ENG-006]
// Regression: Bun.serve({port: 0}) must expose the OS-assigned port on
// server.port. The listen callback (bun_serve_listen_cb) writes the bound
// port into `actual_port`; bun_serve reads it back and exposes it. This
// test asserts the data-race-free channel (AtomicU16 Acquire/Release).
#[test]
fn bun_serve_actual_port_dynamic_bind_channel() {
let data = BunServeUserData {
fetch_cb_key: None,
websocket_cb_key: None,
app_ptr: ::std::ptr::null_mut(),
hostname: "0.0.0.0".to_string(),
port: 0,
actual_port: AtomicU16::new(0),
cx: ::std::ptr::null_mut(),
};
// Before listen: fall back to requested port (0).
assert_eq!(data.actual_port.load(Ordering::Acquire), 0);
// Simulate the listen callback writing the OS-assigned port.
data.actual_port.store(54321, Ordering::Release);
let bound = data.actual_port.load(Ordering::Acquire);
assert_eq!(bound, 54321);
// bun_serve's exposure logic: use bound_port when > 0, else requested.
let exposed = if bound > 0 { bound } else { data.port };
assert_eq!(exposed, 54321);
assert!(
exposed > 0,
"BCE-005: server.port must be > 0 after dynamic bind"
);
}
// ── init_process_start ──
#[test]
fn init_process_start_sets_instant() {
init_process_start();
PROCESS_START.with(|s| {
let start = *s.borrow();
assert!(start.is_some());
});
}
#[test]
fn init_process_start_idempotent() {
init_process_start();
let first = PROCESS_START.with(|s| s.borrow().unwrap());
init_process_start();
let second = PROCESS_START.with(|s| s.borrow().unwrap());
// Second call resets the instant, so second >= first
assert!(second >= first);
}
// ── Hash computation (sha256/sha512) ──
#[test]
fn sha256_empty_input() {
let mut hasher = bun_sha_hmac::SHA256::init();
hasher.update(b"");
let mut result = [0u8; bun_sha_hmac::SHA256::DIGEST];
hasher.r#final(&mut result);
assert_eq!(result.len(), 32);
let hex: String = result.iter().map(|b| format!("{:02x}", b)).collect();
assert!(hex.starts_with("e3b0c442"));
}
#[test]
fn sha256_hello_world() {
let mut hasher = bun_sha_hmac::SHA256::init();
hasher.update(b"hello world");
let mut result = [0u8; bun_sha_hmac::SHA256::DIGEST];
hasher.r#final(&mut result);
let hex: String = result.iter().map(|b| format!("{:02x}", b)).collect();
assert!(hex.starts_with("b94d27b9"));
}
#[test]
fn sha512_empty_input() {
let mut hasher = bun_sha_hmac::SHA512::init();
hasher.update(b"");
let mut result = [0u8; bun_sha_hmac::SHA512::DIGEST];
hasher.r#final(&mut result);
assert_eq!(result.len(), 64);
let hex: String = result.iter().map(|b| format!("{:02x}", b)).collect();
assert!(hex.starts_with("cf83e135"));
}
#[test]
fn sha512_hello_world() {
let mut hasher = bun_sha_hmac::SHA512::init();
hasher.update(b"hello world");
let mut result = [0u8; bun_sha_hmac::SHA512::DIGEST];
hasher.r#final(&mut result);
let hex: String = result.iter().map(|b| format!("{:02x}", b)).collect();
assert!(hex.starts_with("309ecc48"));
}
#[test]
fn hash_hex_format_lowercase() {
let mut hasher = bun_sha_hmac::SHA256::init();
hasher.update(b"\xff");
let mut result = [0u8; bun_sha_hmac::SHA256::DIGEST];
hasher.r#final(&mut result);
let hex: String = result.iter().map(|b| format!("{:02x}", b)).collect();
assert_eq!(hex, hex.to_lowercase());
}
#[test]
fn sha256_deterministic() {
let mut h1 = bun_sha_hmac::SHA256::init();
h1.update(b"test data");
let mut r1 = [0u8; bun_sha_hmac::SHA256::DIGEST];
h1.r#final(&mut r1);
let mut h2 = bun_sha_hmac::SHA256::init();
h2.update(b"test data");
let mut r2 = [0u8; bun_sha_hmac::SHA256::DIGEST];
h2.r#final(&mut r2);
assert_eq!(r1.as_slice(), r2.as_slice());
}
#[test]
fn sha256_different_inputs_different_outputs() {
let mut h1 = bun_sha_hmac::SHA256::init();
h1.update(b"input1");
let mut r1 = [0u8; bun_sha_hmac::SHA256::DIGEST];
h1.r#final(&mut r1);
let mut h2 = bun_sha_hmac::SHA256::init();
h2.update(b"input2");
let mut r2 = [0u8; bun_sha_hmac::SHA256::DIGEST];
h2.r#final(&mut r2);
assert_ne!(r1.as_slice(), r2.as_slice());
}
#[test]
fn sha256_incremental_update() {
let mut h1 = bun_sha_hmac::SHA256::init();
h1.update(b"hello");
h1.update(b" world");
let mut r1 = [0u8; bun_sha_hmac::SHA256::DIGEST];
h1.r#final(&mut r1);
let mut h2 = bun_sha_hmac::SHA256::init();
h2.update(b"hello world");
let mut r2 = [0u8; bun_sha_hmac::SHA256::DIGEST];
h2.r#final(&mut r2);
assert_eq!(r1.as_slice(), r2.as_slice());
}
#[test]
fn sha256_large_input() {
let data = vec![0xABu8; 10_000];
let mut hasher = bun_sha_hmac::SHA256::init();
hasher.update(&data);
let mut result = [0u8; bun_sha_hmac::SHA256::DIGEST];
hasher.r#final(&mut result);
assert_eq!(result.len(), 32);
}
#[test]
fn sha512_large_input() {
let data = vec![0xCDu8; 10_000];
let mut hasher = bun_sha_hmac::SHA512::init();
hasher.update(&data);
let mut result = [0u8; bun_sha_hmac::SHA512::DIGEST];
hasher.r#final(&mut result);
assert_eq!(result.len(), 64);
}
}