Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
26 changes: 26 additions & 0 deletions src/bun_core/string/immutable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,12 @@ 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 {
// `check_len = true`: an empty suffix is a match, not a comparator debug assert.
self_.len() >= suffix.len()
&& eql_case_insensitive_ascii(&self_[self_.len() - suffix.len()..], suffix, true)
}
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 Expand Up @@ -2739,6 +2745,26 @@ mod tests {
assert!(!super::eql_case_insensitive_ascii(b"Ab", b"a", true));
}

#[test]
fn ends_with_case_insensitive_ascii_handles_empty_and_oversized_suffixes() {
assert!(super::ends_with_case_insensitive_ascii(
b"bunx.EXE",
b"bunx.exe"
));
assert!(super::ends_with_case_insensitive_ascii(
b"C:\\bin\\BUNX.EXE",
b"bunx.exe"
));
assert!(super::ends_with_case_insensitive_ascii(b"BUNX", b"bunx"));
assert!(!super::ends_with_case_insensitive_ascii(
b"bun.exe", b"bunx"
));
assert!(!super::ends_with_case_insensitive_ascii(b"bun", b"bunx"));
assert!(super::ends_with_case_insensitive_ascii(b"bunx", b""));
assert!(super::ends_with_case_insensitive_ascii(b"", b""));
assert!(!super::ends_with_case_insensitive_ascii(b"", b"bunx"));
}

#[test]
fn convert_utf8_to_utf16_in_buffer_fallback_rejects_malformed_sequences() {
let mut buf = [0u16; 16];
Expand Down
11 changes: 7 additions & 4 deletions src/runtime/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -824,25 +824,28 @@ pub mod command {
// `bun_clap::streaming::WARN_ON_UNRECOGNIZED_FLAG` so node-mode argv parsing
// stays silent on unknown flags.
// ──────────────────
// Case-insensitive: `PATHEXT` resolution can yield `bunx.EXE` (#36826).
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