Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ pub struct VirtualMachine {
// `transpiler.resolver.standalone_module_graph` without a downcast.
pub standalone_module_graph: Option<&'static dyn bun_resolver::StandaloneModuleGraph>,
pub smol: bool,
/// Node's `getOptionValue('--eval')` has a value for this VM.
pub has_eval_string: bool,
// LAYERING: real type is `bun_runtime::dns_jsc::Order` (forward
// dep); stored as its `u8` repr.
pub dns_result_order: u8,
Expand Down Expand Up @@ -2782,13 +2784,15 @@ extern crate alloc;
/// casts back on the other side of each hook.
pub type RuntimeState = *mut c_void;

/// Runtime flags a Worker's `execArgv` can set. `true` means allowed.
/// Runtime flags a Worker's `execArgv` can set.
#[derive(Copy, Clone, Debug)]
pub struct WorkerExecArgvFlags {
/// `!--no-addons`
pub allow_addons: bool,
/// `!--no-ffi-cc`
pub allow_ffi_cc: bool,
/// `--eval` has a value. See [`VirtualMachine::has_eval_string`].
pub has_eval_string: bool,
/// Where a flag is that is the process's, which a Worker cannot be given
/// (`ERR_WORKER_INVALID_EXEC_ARGV`): `--disallow-code-generation-from-strings`.
pub invalid: Option<usize>,
Expand Down Expand Up @@ -4860,6 +4864,7 @@ impl VirtualMachine {
// executable) resolve against the real filesystem and fail.
vm_ref.transpiler.resolver.standalone_module_graph = opts.graph;
vm_ref.hot_reload = worker.hot_reload();
vm_ref.has_eval_string = worker.has_eval_string();
vm_ref.initial_script_execution_context_identifier = worker.execution_context_id() as i32;
if opts.graph.is_none() {
vm_ref.transpiler.configure_linker();
Expand Down
11 changes: 0 additions & 11 deletions src/jsc/bindings/BunProcess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3219,17 +3219,6 @@ JSC_DEFINE_CUSTOM_SETTER(setProcessArgv, (JSGlobalObject * globalObject, Encoded
return true;
}

extern "C" EncodedJSValue Bun__Process__getExecArgv(JSGlobalObject* lexicalGlobalObject)
{
auto* globalObject = defaultGlobalObject(lexicalGlobalObject);
auto* process = globalObject->processObject();
if (!process) {
return JSValue::encode(jsUndefined());
}

return JSValue::encode(process->getExecArgv(globalObject));
}

JSC_DEFINE_CUSTOM_GETTER(processExecArgv, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName))
{
Process* process = getProcessObject(globalObject, JSValue::decode(thisValue));
Expand Down
10 changes: 10 additions & 0 deletions src/jsc/web_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ pub struct WebWorker {
exec_argv_ptr: *const WTFStringImpl,
exec_argv_len: usize,
inherit_exec_argv: bool,
/// The worker VM's [`VirtualMachine::has_eval_string`].
has_eval_string: bool,
unresolved_specifier: Box<[u8]>,
preloads: Vec<Box<[u8]>>,
name: bun_core::ZBox,
Expand Down Expand Up @@ -352,6 +354,7 @@ impl WebWorker {
// SAFETY: `parent` is the calling thread's live VM.
let parent_ref = unsafe { &*parent };
let mut transform_options = (*parent_ref.transpiler.options.transform_options).clone();
let mut has_eval_string = parent_ref.has_eval_string;
if !inherit_exec_argv {
let hooks = runtime_hooks().expect("RuntimeHooks not installed");
// SAFETY: caller passed valid (ptr,len) borrowed from C++ WorkerOptions;
Expand All @@ -362,6 +365,7 @@ impl WebWorker {
exec_argv_len,
))
};
has_eval_string = parsed.is_some_and(|flags| flags.has_eval_string);
Comment thread
robobun marked this conversation as resolved.
Resolved
if let Some(flags) = parsed {
if let Some(invalid) = flags.invalid {
use bun_core::WTFStringImplExt as _;
Expand Down Expand Up @@ -420,6 +424,7 @@ impl WebWorker {
exec_argv_ptr,
exec_argv_len,
inherit_exec_argv,
has_eval_string,
unresolved_specifier: spec_slice.slice().to_vec().into_boxed_slice(),
preloads,
name: if name_str.is_empty() {
Expand Down Expand Up @@ -586,6 +591,11 @@ impl WebWorker {
self.hot_reload
}

#[inline]
pub(crate) fn has_eval_string(&self) -> bool {
self.has_eval_string
}

#[inline]
pub(crate) fn arm_test_gate(&self) -> bool {
self.arm_test_gate
Expand Down
4 changes: 4 additions & 0 deletions src/runtime/cli/run_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1080,6 +1080,10 @@ Full documentation is available at <magenta>https://bun.com/docs/cli/run<r>
Some(Box::new(bun_ast::Source::init_path_string(entry, script)));
vm.module_loader.interactive_eval_script =
ctx.runtime_options.eval.interactive_script.take();
let interactive = vm.module_loader.interactive_eval_script.as_deref();
let has_code = interactive.is_none_or(|code| !code.is_empty());
// `bun run -e <code> <file>` runs the file, and `--interactive` can have no `-e` code.
vm.has_eval_string = entry.ends_with(EVAL_TRIGGER) && has_code;
if ctx.runtime_options.eval.eval_and_print {
vm.transpiler.options.dead_code_elimination = false;
}
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
Expand Down
60 changes: 60 additions & 0 deletions src/runtime/jsc_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1563,9 +1563,15 @@ unsafe fn apply_standalone_runtime_flags(
unsafe fn parse_worker_exec_argv_flags(
exec_argv: &[bun_core::WTFStringImpl],
) -> Option<WorkerExecArgvFlags> {
// SAFETY: per fn contract, a non-null entry is a live `WTFStringImpl*`.
let tokens = exec_argv
.iter()
.filter(|arg| !arg.is_null())
.map(|&arg| unsafe { &*arg });
let mut flags = WorkerExecArgvFlags {
allow_addons: true,
allow_ffi_cc: true,
has_eval_string: exec_argv_has_eval_string(tokens),
invalid: None,
};
for (index, &arg) in exec_argv.iter().enumerate() {
Expand Down Expand Up @@ -1595,6 +1601,60 @@ unsafe fn parse_worker_exec_argv_flags(
Some(flags)
}

/// Whether `--eval` has a value as Node's parser reads `tokens`: a token after a flag can be that flag's value.
fn exec_argv_has_eval_string<'a>(
tokens: impl Iterator<Item = &'a bun_core::WTFStringImplStruct>,
) -> bool {
let is_flag = |token: &bun_core::WTFStringImplStruct| {
if token.is_8bit() {
token.latin1_slice().first() == Some(&b'-')
} else {
token.utf16_slice().first() == Some(&u16::from(b'-'))
}
};
let mut tokens = tokens.peekable();
let mut has_eval_string = false;
let mut value_may_follow = false;
while let Some(token) = tokens.next() {
if !is_flag(token) {
if !core::mem::take(&mut value_may_follow) {
break;
}
continue;
}
let flag = token.to_utf8();
let flag: &[u8] = &flag;
if flag == b"--" {
break;
}
value_may_follow = false;
let with_code = [b"--eval=".as_slice(), b"--print=", b"-e=", b"-p="];
if let Some(code) = with_code
.iter()
.find_map(|prefix| flag.strip_prefix(*prefix))
{
has_eval_string = !code.is_empty();
} else if matches!(flag, b"-e" | b"--eval" | b"-pe") {
// Node rejects a code that starts with `-`. Bun's CLI takes it, so it is the code here too.
if let Some(code) = tokens.next() {
has_eval_string = code.length() != 0;
}
} else if matches!(flag, b"-p" | b"--print") {
// https://github.com/nodejs/node/blob/v26.3.0/src/node_options.cc#L1171-L1174
has_eval_string |= tokens
.next_if(|next| next.length() != 0 && !is_flag(next))
.is_some();
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
} else {
let takes_no_value = matches!(
flag,
b"--no-addons" | b"--no-ffi-cc" | b"--disallow-code-generation-from-strings"
);
value_may_follow = !takes_no_value && !bun_core::strings::contains_char(flag, b'=');
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
}
has_eval_string
}

/// `jsc.API.cron.CronJob.clearAllForVM(vm, .teardown)` —
/// stops every in-process `Bun.cron()` job registered on
/// this VM and releases the pending-promise ref so the struct frees (the event
Expand Down
8 changes: 1 addition & 7 deletions src/runtime/node/node_process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@ use bun_core::env_var;
use bun_core::{self, Environment, Global};
use bun_jsc::{EncodedSliceJsc as _, JSGlobalObject, JSValue, JsResult};

// Both materialize the array on first access through `Bun__Process__createArgv`
// / `createExecArgv` below, which return zero with the exception pending.
// Filled on first access by `Bun__Process__createArgv` below. Zero means a pending exception.
unsafe extern "C" {
safe fn Bun__Process__getArgv(global: &JSGlobalObject) -> JSValue;
safe fn Bun__Process__getExecArgv(global: &JSGlobalObject) -> JSValue;
}

// ───────────────────────────── argv0 / execPath ─────────────────────────────
Expand Down Expand Up @@ -58,10 +56,6 @@ pub(crate) fn get_argv(global: &JSGlobalObject) -> JsResult<JSValue> {
bun_jsc::call_zero_is_throw(global, || Bun__Process__getArgv(global))
}

pub(crate) fn get_exec_argv(global: &JSGlobalObject) -> JsResult<JSValue> {
bun_jsc::call_zero_is_throw(global, || Bun__Process__getExecArgv(global))
}

// ───────────────────────────── exit ─────────────────────────────

// @190n: this may need to be noreturn
Expand Down
103 changes: 53 additions & 50 deletions src/runtime/node/util/parse_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,50 @@ use super::validators;

bun_output::declare_scope!(parseArgs, hidden);

/// Represents a slice of a JSValue array
#[derive(Copy, Clone)]
struct ArgsSlice {
array: JSValue,
start: u32,
end: u32,
}
mod args_slice {
use bun_jsc::{JSGlobalObject, JSValue, JsResult};

/// A slice of a JSValue array. Private fields, so `start <= end` always holds.
#[derive(Copy, Clone)]
pub(super) struct ArgsSlice {
array: JSValue,
start: u32,
end: u32,
}

impl ArgsSlice {
pub(super) const EMPTY: ArgsSlice = ArgsSlice {
array: JSValue::UNDEFINED,
start: 0,
end: 0,
};

/// `array[start..]`, clamped as `ArrayPrototypeSlice(array, start)` is.
pub(super) fn new(
global: &JSGlobalObject,
array: JSValue,
start: u32,
) -> JsResult<ArgsSlice> {
let end = u32::try_from(array.get_length(global)?).expect("int cast");
Ok(ArgsSlice {
array,
start: start.min(end),
end,
})
}

#[inline]
pub(super) fn len(&self) -> u32 {
self.end - self.start
}

impl ArgsSlice {
#[inline]
fn get(&self, global: &JSGlobalObject, i: u32) -> JsResult<JSValue> {
self.array.get_index(global, self.start + i)
#[inline]
pub(super) fn get(&self, global: &JSGlobalObject, i: u32) -> JsResult<JSValue> {
self.array.get_index(global, self.start + i)
}
}
}
use args_slice::ArgsSlice;

/// Helper ref to either a JSValue or a String,
/// used in order to avoid creating unneeded JSValue as much as possible
Expand Down Expand Up @@ -184,40 +214,17 @@ fn find_option_by_long_name(long_name: &String, options: &[OptionDefinition]) ->

/// Gets the default args from the process argv
fn get_default_args(global: &JSGlobalObject) -> JsResult<ArgsSlice> {
// Work out where to slice process.argv for user supplied arguments

let exec_argv = super::process::get_exec_argv(global)?;
let argv = super::process::get_argv(global)?;
if argv.is_array() && exec_argv.is_array() {
let mut iter = exec_argv.array_iterator(global)?;
while let Some(item) = iter.next()? {
if item.is_string() {
let str = item.to_bun_string(global)?;
if str.eq_ascii(b"-e")
|| str.eq_ascii(b"--eval")
|| str.eq_ascii(b"-p")
|| str.eq_ascii(b"--print")
{
return Ok(ArgsSlice {
array: argv,
start: 1,
end: u32::try_from(argv.get_length(global)?).expect("int cast"),
});
}
}
}
return Ok(ArgsSlice {
array: argv,
start: 2,
end: u32::try_from(argv.get_length(global)?).expect("int cast"),
});
if !argv.is_array() {
return Ok(ArgsSlice::EMPTY);
}

Ok(ArgsSlice {
array: JSValue::UNDEFINED,
start: 0,
end: 0,
})
// https://github.com/nodejs/node/blob/v26.3.0/lib/internal/util/parse_args/parse_args.js#L58-L67
let start = if global.bun_vm().has_eval_string {
1
} else {
2
};
ArgsSlice::new(global, argv, start)
}

/// In strict mode, throw for possible usage errors like "--foo --bar" where foo was defined as a string-valued arg
Expand Down Expand Up @@ -524,7 +531,7 @@ fn tokenize_args(
args: ArgsSlice,
options: &[OptionDefinition],
) -> JsResult<()> {
let num_args: u32 = args.end - args.start;
let num_args = args.len();
let mut index: u32 = 0;
while index < num_args {
let arg_ref = ValueRef::Jsvalue(args.get(global, index)?);
Expand Down Expand Up @@ -905,11 +912,7 @@ fn parse_args_impl(
};
let args: ArgsSlice = if !config_args.is_undefined_or_null() {
validators::validate_array(global, config_args, "args", None)?;
ArgsSlice {
array: config_args,
start: 0,
end: u32::try_from(config_args.get_length(global)?).expect("int cast"),
}
ArgsSlice::new(global, config_args, 0)?
} else {
get_default_args(global)?
};
Expand Down Expand Up @@ -983,7 +986,7 @@ fn parse_args_impl(
bun_output::scoped_log!(
parseArgs,
"Phase 1+2: tokenize args (args.len={})",
args.end - args.start
args.len()
);

// note that "values" needs to have a null prototype instead of Object, to avoid issues such as "values.toString"` being defined
Expand Down
5 changes: 5 additions & 0 deletions test/js/node/test/fixtures/parse-args.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const { parseArgs } = require('util');

const parsedArgs = parseArgs({ strict: false }).values;

process.stdout.write(JSON.stringify(parsedArgs));
Loading
Loading