Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 1 addition & 3 deletions src/bunfig/bunfig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1544,9 +1544,7 @@ impl<'a> Parser<'a> {
let remap = |e: FromExprError| -> crate::Error {
match e {
FromExprError::OutOfMemory => crate::Error::Alloc(bun_alloc::AllocError),
FromExprError::UnexpectedExpr | FromExprError::InvalidRegExp => {
crate::Error::InvalidBunfig
}
FromExprError::UnexpectedExpr => crate::Error::InvalidBunfig,
}
};
if let Some(public_hoist_pattern_expr) = install_obj.get(b"publicHoistPattern") {
Expand Down
49 changes: 6 additions & 43 deletions src/ini/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1500,8 +1500,7 @@ mod draft {
match pnpm_matcher_from_expr(&public_hoist_pattern_expr, log, source, bump) {
Ok(v) => Some(v),
Err(FromExprError::OutOfMemory) => return Err(AllocError),
Err(_) => {
// error.InvalidRegExp, error.UnexpectedExpr
Err(FromExprError::UnexpectedExpr) => {
log.reset();
None
}
Expand All @@ -1513,8 +1512,7 @@ mod draft {
match pnpm_matcher_from_expr(&hoist_pattern_expr, log, source, bump) {
Ok(v) => Some(v),
Err(FromExprError::OutOfMemory) => return Err(AllocError),
Err(_) => {
// error.InvalidRegExp, error.UnexpectedExpr
Err(FromExprError::UnexpectedExpr) => {
log.reset();
None
}
Expand Down Expand Up @@ -1633,8 +1631,8 @@ mod draft {
}

use bun_install_types::NodeLinker::{
Behavior as PnpmBehavior, CreateMatcherError, FromExprError, Matcher as PnpmMatcherEntry,
PnpmMatcher, create_matcher,
Behavior as PnpmBehavior, FromExprError, Matcher as PnpmMatcherEntry, PnpmMatcher,
create_matcher,
};

/// `PnpmMatcher.fromExpr` operating on
Expand All @@ -1651,57 +1649,22 @@ mod draft {
source: &Source,
bump: &Arena,
) -> Result<PnpmMatcher, FromExprError> {
let mut buf: Vec<u8> = Vec::new();

let mut matchers: Vec<PnpmMatcherEntry> = Vec::new();
let mut has_include = false;
let mut has_exclude = false;

match &expr.data {
ExprData::EString(s) => {
let mut s = *s;
let pattern = s.slice(bump);
let matcher = match create_matcher(pattern, &mut buf) {
Ok(m) => m,
Err(CreateMatcherError::OutOfMemory) => return Err(FromExprError::OutOfMemory),
Err(CreateMatcherError::InvalidRegExp) => {
log.add_error_fmt_opts(
format_args!("Invalid regex: {}", bstr::BStr::new(pattern)),
bun_ast::AddErrorOptions {
loc: expr.loc,
redact_sensitive_information: true,
source: Some(source),
..Default::default()
},
);
return Err(FromExprError::InvalidRegExp);
}
};
let matcher = create_matcher(s.slice(bump));
has_include = has_include || !matcher.is_exclude;
has_exclude = has_exclude || matcher.is_exclude;
matchers.push(matcher);
}
ExprData::EArray(patterns) => {
for pattern_expr in patterns.items.slice() {
if let Some(pattern) = pattern_expr.as_string_cloned(bump)? {
let matcher = match create_matcher(pattern, &mut buf) {
Ok(m) => m,
Err(CreateMatcherError::OutOfMemory) => {
return Err(FromExprError::OutOfMemory);
}
Err(CreateMatcherError::InvalidRegExp) => {
log.add_error_fmt_opts(
format_args!("Invalid regex: {}", bstr::BStr::new(pattern)),
bun_ast::AddErrorOptions {
loc: pattern_expr.loc,
redact_sensitive_information: true,
source: Some(source),
..Default::default()
},
);
return Err(FromExprError::InvalidRegExp);
}
};
let matcher = create_matcher(pattern);
has_include = has_include || !matcher.is_exclude;
has_exclude = has_exclude || matcher.is_exclude;
matchers.push(matcher);
Expand Down
148 changes: 58 additions & 90 deletions src/install_types/NodeLinker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ pub mod npm {
// (`__bun_regex_*`) defined `#[no_mangle]` in `bun_jsc::regular_expression`.
// ══════════════════════════════════════════════════════════════════════════

use core::cell::OnceCell;
use core::ptr::NonNull;

use bun_alloc::Arena;
Expand All @@ -85,42 +86,59 @@ use bun_core::{String as BunString, strings};
// `bun_jsc::regular_expression`; declared here as `extern "Rust"` and
// resolved at link time.
unsafe extern "Rust" {
/// Compile `pattern` with no flags. `None` ⇔ `error.InvalidRegExp`.
/// Performs `jsc::initialize` lazily on first call.
/// Compile `pattern` with no flags. `None` ⇔ the pattern does not compile.
/// Initializes JSC first (idempotent), so it must only run from
/// [`RegularExpression::matches`]; see the type's docs.
fn __bun_regex_compile(pattern: BunString) -> Option<NonNull<()>>;
fn __bun_regex_matches(regex: NonNull<()>, input: &BunString) -> bool;
fn __bun_regex_drop(regex: NonNull<()>);
}

/// Owned, type-erased JSC regex; drops through the vtable.
/// Owned, type-erased compiled JSC regex; drops through the vtable.
// FORWARD_DECL(b0): bun_jsc::RegularExpression — stored as raw NonNull<()>
// (NOT Box<ZST>: a zero-sized opaque Box is a dangling sentinel that would
// leak the real JSC allocation and skip its destructor).
pub struct RegularExpression(NonNull<()>);
struct CompiledRegularExpression(NonNull<()>);

impl RegularExpression {
#[inline]
fn matches(&self, input: &BunString) -> bool {
// SAFETY: self.0 was produced by `__bun_regex_compile`.
unsafe { __bun_regex_matches(self.0, input) }
}
}

impl Drop for RegularExpression {
impl Drop for CompiledRegularExpression {
fn drop(&mut self) {
// SAFETY: self.0 was produced by `__bun_regex_compile`; runs JSC destructor + free.
unsafe { __bun_regex_drop(self.0) }
}
}

/// Compile `pattern` into a Yarr regex via the link-time extern. `pub` so
/// higher-tier callers re-use this single declaration site instead of
/// duplicating the `__bun_regex_*` extern block (one declarer per upward call,
/// per PORTING.md §extern-Rust-ban).
#[inline]
fn compile_regex(pattern: BunString) -> Option<RegularExpression> {
// SAFETY: link-time extern; pattern ownership transfers.
unsafe { __bun_regex_compile(pattern) }.map(RegularExpression)
/// A package-name pattern, compiled on first use.
///
/// Matchers are built while bunfig.toml / .npmrc load, before the command
/// initializes JSC. `__bun_regex_compile` initializes JSC, and the first
/// `jsc::initialize` of the process fixes JSC's options, so compiling here
/// would drop the options the command passes later (`bun -p` needs eval mode
/// to keep the script's completion value, `bun test --isolate` its JIT
/// thresholds). Only the install linker matches, so compiling there leaves
/// the first call to the command.
pub struct RegularExpression {
/// The anchored pattern built by [`create_matcher`].
source: Box<[u8]>,
/// `None` if the pattern failed to compile; it then never matches. The
/// escaping in [`create_matcher`] leaves Yarr's pattern size limit as the
/// only way to fail.
compiled: OnceCell<Option<CompiledRegularExpression>>,
}

impl RegularExpression {
fn matches(&self, input: &BunString) -> bool {
let compiled = self.compiled.get_or_init(|| {
// SAFETY: link-time extern; pattern ownership transfers.
unsafe { __bun_regex_compile(BunString::clone_utf8(&self.source)) }
.map(CompiledRegularExpression)
});
match compiled {
// SAFETY: `regex.0` was produced by `__bun_regex_compile` and stays
// live until `regex` drops.
Some(regex) => unsafe { __bun_regex_matches(regex.0, input) },
None => false,
}
}
}

pub struct PnpmMatcher {
Expand Down Expand Up @@ -148,7 +166,6 @@ pub enum Behavior {
#[derive(Debug, strum::IntoStaticStr)]
pub enum FromExprError {
OutOfMemory,
InvalidRegExp,
UnexpectedExpr,
}
bun_core::impl_tag_error!(FromExprError);
Expand All @@ -166,10 +183,8 @@ impl PnpmMatcher {
log: &mut bun_ast::Log,
source: &bun_ast::Source,
) -> Result<PnpmMatcher, FromExprError> {
let mut buf: Vec<u8> = Vec::new();
// Scratch arena for `E::String::slice` / `as_string_cloned`.
// Freed on return; the patterns are consumed by
// `create_matcher` before then.
// Freed on return; `create_matcher` copies the patterns before then.
let arena = Arena::new();

let mut matchers: Vec<Matcher> = Vec::new();
Expand All @@ -178,48 +193,15 @@ impl PnpmMatcher {

match expr.data {
ast::ExprData::EString(mut s) => {
let pattern = s.slice(&arena);
let matcher = match create_matcher(pattern, &mut buf) {
Ok(m) => m,
Err(CreateMatcherError::OutOfMemory) => return Err(FromExprError::OutOfMemory),
Err(CreateMatcherError::InvalidRegExp) => {
log.add_error_fmt_opts(
format_args!("Invalid regex: {}", bstr::BStr::new(pattern)),
bun_ast::AddErrorOptions {
loc: expr.loc,
redact_sensitive_information: true,
source: Some(source),
..Default::default()
},
);
return Err(FromExprError::InvalidRegExp);
}
};
let matcher = create_matcher(s.slice(&arena));
has_include = has_include || !matcher.is_exclude;
has_exclude = has_exclude || matcher.is_exclude;
matchers.push(matcher);
}
ast::ExprData::EArray(patterns) => {
for pattern_expr in patterns.slice() {
if let Some(pattern) = pattern_expr.as_string_cloned(&arena)? {
let matcher = match create_matcher(pattern, &mut buf) {
Ok(m) => m,
Err(CreateMatcherError::OutOfMemory) => {
return Err(FromExprError::OutOfMemory);
}
Err(CreateMatcherError::InvalidRegExp) => {
log.add_error_fmt_opts(
format_args!("Invalid regex: {}", bstr::BStr::new(pattern)),
bun_ast::AddErrorOptions {
loc: pattern_expr.loc,
redact_sensitive_information: true,
source: Some(source),
..Default::default()
},
);
return Err(FromExprError::InvalidRegExp);
}
};
let matcher = create_matcher(pattern);
has_include = has_include || !matcher.is_exclude;
has_exclude = has_exclude || matcher.is_exclude;
matchers.push(matcher);
Expand Down Expand Up @@ -321,18 +303,7 @@ impl PnpmMatcher {
}
}

#[derive(Debug, strum::IntoStaticStr)]
pub enum CreateMatcherError {
OutOfMemory,
InvalidRegExp,
}
bun_core::impl_tag_error!(CreateMatcherError);

bun_core::oom_from_alloc!(CreateMatcherError);

pub fn create_matcher(raw: &[u8], buf: &mut Vec<u8>) -> Result<Matcher, CreateMatcherError> {
buf.clear();

pub fn create_matcher(raw: &[u8]) -> Matcher {
let mut trimmed = strings::trim(raw, &strings::WHITESPACE_CHARS);

let mut is_exclude = false;
Expand All @@ -342,27 +313,24 @@ pub fn create_matcher(raw: &[u8], buf: &mut Vec<u8>) -> Result<Matcher, CreateMa
}

if trimmed == b"*" {
return Ok(Matcher {
return Matcher {
pattern: Pattern::MatchAll,
is_exclude,
});
};
}

// Vec::push aborts on
// OOM under the global mimalloc allocator, so no error mapping is needed.
// `escape_reg_exp_*` writes through
// `io::Write` for `Vec<u8>`, which is infallible.
buf.push(b'^');
let _ = escape_reg_exp_for_package_name_matching(trimmed, buf);
buf.push(b'$');

// `__bun_regex_compile` is a link-time extern (cold path) and performs
// `jsc::initialize` before compiling.
let regex = compile_regex(BunString::clone_utf8(buf.as_slice()))
.ok_or(CreateMatcherError::InvalidRegExp)?;

Ok(Matcher {
pattern: Pattern::Regex(regex),
// `escape_reg_exp_*` writes through `io::Write` for `Vec<u8>`, which is
// infallible.
let mut source: Vec<u8> = Vec::with_capacity(trimmed.len() + 2);
source.push(b'^');
let _ = escape_reg_exp_for_package_name_matching(trimmed, &mut source);
source.push(b'$');

Matcher {
pattern: Pattern::Regex(RegularExpression {
source: source.into_boxed_slice(),
compiled: OnceCell::new(),
}),
is_exclude,
})
}
}
10 changes: 5 additions & 5 deletions src/jsc/RegularExpression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,11 @@ impl RegularExpression {

#[unsafe(no_mangle)]
fn __bun_regex_compile(pattern: BunString) -> Option<core::ptr::NonNull<()>> {
// bunfig's hoistPattern is compiled during config load, before the CLI initializes JSC itself.
crate::initialize(crate::InitializeOptions {
one_shot: crate::is_one_shot_eval_invocation(),
..Default::default()
});
// Idempotent. Only the install commands reach this before initializing
// JSC themselves, and the defaults are right for them; every other command
// has already initialized JSC with its own options by the time a matcher
// first runs (see `bun_install_types::NodeLinker::RegularExpression`).
crate::initialize(crate::InitializeOptions::default());
match RegularExpression::init(pattern, Flags::None) {
Ok(r) => core::ptr::NonNull::new(r.cast()),
Err(_) => None,
Expand Down
30 changes: 29 additions & 1 deletion test/cli/run/run-eval.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { SyncSubprocess } from "bun";
import { describe, expect, test } from "bun:test";
import { rmSync, writeFileSync } from "fs";
import { bunEnv, bunExe, isWindows, tmpdirSync } from "harness";
import { bunEnv, bunExe, isWindows, tempDir, tmpdirSync } from "harness";
import { tmpdir } from "os";
import { join, sep } from "path";

Expand Down Expand Up @@ -173,6 +173,34 @@ describe("--print for cjs/esm", () => {
expect(stdout.toString()).toBe("true\n");
expect(exitCode).toBe(0);
});

// The hoist patterns in [install] are regexes. Compiling them while
// bunfig.toml loaded initialized JSC before `bun -p` could turn on eval
// mode, so the script's completion value was dropped and every --print
// printed undefined.
describe.each(["hoistPattern", "publicHoistPattern"])("with install.%s in bunfig.toml", key => {
test.concurrent.each([
{ expr: "Math.max(1, 9)", expected: "9" },
{ expr: "[1, 2].map(x => x * 2)", expected: "[ 2, 4 ]" },
{ expr: "1 + 1", expected: "2" },
{ expr: "(await 1) + 1", expected: "2" },
])("bun -p $expr", async ({ expr, expected }) => {
using dir = tempDir("eval-install-pattern", {
"bunfig.toml": `[install]\n${key} = ["*eslint*", "!eslint-plugin-*"]\n`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "-p", expr],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout).toBe(`${expected}\n`);
expect(exitCode).toBe(0);
});
});
});

function group(run: (code: string) => SyncSubprocess<"pipe", "inherit">) {
Expand Down
Loading
Loading