Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
5 changes: 5 additions & 0 deletions src/bun_core/string/immutable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,11 @@ pub fn starts_with_case_insensitive_ascii(self_: &[u8], prefix: &[u8]) -> bool {
&& eql_case_insensitive_ascii(&self_[0..prefix.len()], prefix, false)
}

pub fn ends_with_case_insensitive_ascii(self_: &[u8], suffix: &[u8]) -> bool {
self_.len() >= suffix.len()
&& eql_case_insensitive_ascii(&self_[self_.len() - suffix.len()..], suffix, false)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

pub use crate::strings_impl::{
has_prefix_t, has_prefix_t as starts_with_generic, has_suffix_t,
has_suffix_t as ends_with_generic,
Expand Down
13 changes: 9 additions & 4 deletions src/runtime/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -812,25 +812,30 @@ pub mod command {
// `bun_clap::streaming::WARN_ON_UNRECOGNIZED_FLAG` so node-mode argv parsing
// stays silent on unknown flags.
// ──────────────────
// Matched case-insensitively: Windows resolves executables
// case-insensitively (`PATHEXT` commonly yields `bunx.EXE`, #36826) and
// macOS filesystems are case-insensitive by default.
Comment thread
robobun marked this conversation as resolved.
Outdated
fn is_bun_x(argv0: &[u8]) -> bool {
#[cfg(windows)]
{
return strings::ends_with(argv0, b"bunx.exe") || strings::ends_with(argv0, b"bunx");
return strings::ends_with_case_insensitive_ascii(argv0, b"bunx.exe")
|| strings::ends_with_case_insensitive_ascii(argv0, b"bunx");
}
#[cfg(not(windows))]
{
strings::ends_with(argv0, b"bunx")
strings::ends_with_case_insensitive_ascii(argv0, b"bunx")
}
}

fn is_node(argv0: &[u8]) -> bool {
#[cfg(windows)]
{
return strings::ends_with(argv0, b"node.exe") || strings::ends_with(argv0, b"node");
return strings::ends_with_case_insensitive_ascii(argv0, b"node.exe")
|| strings::ends_with_case_insensitive_ascii(argv0, b"node");
}
#[cfg(not(windows))]
{
strings::ends_with(argv0, b"node")
strings::ends_with_case_insensitive_ascii(argv0, b"node")
}
}

Expand Down
34 changes: 34 additions & 0 deletions test/cli/install/bunx.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,40 @@ describe("bunx --no-install", () => {
});
});

// https://github.com/oven-sh/bun/issues/36826
// Windows resolves executables case-insensitively (PATHEXT commonly lists
// `.EXE`), so argv[0] can be `bunx.EXE` for the same on-disk `bunx.exe`.
// The invocation-name match must ignore ASCII case; same for posix, where
// macOS filesystems are case-insensitive by default.
it.concurrent.each(isWindows ? ["bunx.EXE", "BUNX.EXE"] : ["BUNX", "bunX"])(
"detects bunx mode when invoked as %s",
async name => {
const { x_dir, env } = setup();
if (isWindows) {
// On disk the file is lowercase; only the invocation casing differs.
copyFileSync(bunExe(), join(x_dir, "bunx.exe"));
} else {
symlinkSync(bunExe(), join(x_dir, name));
}

await using proc = spawn({
cmd: [join(x_dir, name), "--help"],
cwd: x_dir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env,
});
const [out, err, exited] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

// `bunx --help` writes its usage to stderr and exits 1; misclassified as
// plain `bun`, it would write the full CLI help to stdout and exit 0.
expect(err).toContain("Usage: bunx");
expect(out).not.toContain("Bun is a fast JavaScript runtime");
expect(exited).toBe(1);
},
);

it.concurrent("should handle postinstall scripts correctly with symlinked bunx", async () => {
const { x_dir, env } = setup();
// Create a symlink to bun called "bunx"
Expand Down
27 changes: 26 additions & 1 deletion test/cli/run/as-node.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test";
import { copyFileSync, symlinkSync } from "node:fs";
import { join } from "path";
import { bunEnv, bunExe, fakeNodeRun, tempDir } from "../../harness";
import { bunEnv, bunExe, fakeNodeRun, isWindows, tempDir } from "../../harness";

describe("fake node cli", () => {
test("the node cli actually works", () => {
Expand Down Expand Up @@ -111,4 +112,28 @@ describe("fake node cli", () => {
expect(result.stderr.toString()).toContain("Missing script");
expect(result.success).toBe(false);
});

// https://github.com/oven-sh/bun/issues/36826
// Windows resolves executables case-insensitively (PATHEXT commonly lists
// `.EXE`), so argv[0] can be `node.EXE`; the invocation-name match must
// ignore ASCII case. Misclassified as plain `bun`, the bare invocation
// below would run the empty piped stdin and exit 0 instead of printing
// "Missing script".
test.each(isWindows ? ["node.EXE", "NODE.EXE"] : ["NODE", "nodE"])("detects node mode when invoked as %s", name => {
using temp = tempDir("fake-node-case", {});
const dir = String(temp);
if (isWindows) {
// On disk the file is lowercase; only the invocation casing differs.
copyFileSync(bunExe(), join(dir, "node.exe"));
} else {
symlinkSync(bunExe(), join(dir, name));
}
const result = Bun.spawnSync([join(dir, name)], {
cwd: dir,
env: { ...bunEnv, NODE_ENV: undefined },
stdin: Buffer.alloc(0),
});
expect(result.stderr.toString()).toContain("Missing script");
expect(result.success).toBe(false);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
});
Loading