mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
## Summary Fixes #13316 Fixes #18275 Running `bunx cowsay ""` (or any package with an empty string argument) on Windows caused a panic. Additionally, `bunx concurrently "command with spaces"` was splitting quoted arguments incorrectly. **Repro #13316:** ```bash bunx cowsay "" # panic(main thread): reached unreachable code ``` **Repro #18275:** ```bash bunx concurrently "bun --version" "bun --version" # Only runs once, arguments split incorrectly # Expected: ["bun --version", "bun --version"] # Actual: ["bun", "--version", "bun", "--version"] ``` ## Root Cause The bunx fast path on Windows bypasses libuv and calls `CreateProcessW` directly to save 5-12ms. The command line building logic had two issues: 1. **Empty strings**: Not quoted at all, resulting in invalid command line 2. **Arguments with spaces**: Not quoted, causing them to be split into multiple arguments ## Solution Implement Windows command-line argument quoting using libuv's proven algorithm: - Port of libuv's `quote_cmd_arg` function (process backwards + reverse) - Empty strings become `""` - Strings with spaces/tabs/quotes are wrapped in quotes - Backslashes before quotes are properly escaped per Windows rules **Why not use libuv directly?** - Normal `Bun.spawn()` uses `uv_spawn()` which handles quoting internally - bunx fast path bypasses libuv to save 5-12ms (calls `CreateProcessW` directly) - libuv's `quote_cmd_arg` is a static function (not exported) - Solution: port the algorithm to Zig ## Test Plan - [x] Added regression test for empty strings (#13316) - [x] Added regression test for arguments with spaces (#18275) - [x] Verified system bun (v1.3.3) fails both tests - [x] Verified fix passes both tests - [x] Implementation based on battle-tested libuv algorithm 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <claude-bot@bun.sh> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
157 lines
5.2 KiB
TypeScript
157 lines
5.2 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { bunEnv, bunExe, isWindows, tempDir } from "harness";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
// https://github.com/oven-sh/bun/issues/13316
|
|
// bunx cowsay "" panicked on Windows due to improper handling of empty string arguments
|
|
// The issue was in the BunXFastPath.tryLaunch function which didn't properly quote
|
|
// empty string arguments for the Windows command line.
|
|
describe.if(isWindows)("#13316 - bunx with empty string arguments", () => {
|
|
test("bunx does not panic with empty string argument", async () => {
|
|
// Create a minimal package that echoes its arguments
|
|
using dir = tempDir("issue-13316", {
|
|
"package.json": JSON.stringify({
|
|
name: "test-project",
|
|
version: "1.0.0",
|
|
dependencies: {
|
|
"echo-args-test": "file:./echo-args-test",
|
|
},
|
|
}),
|
|
"echo-args-test/package.json": JSON.stringify({
|
|
name: "echo-args-test",
|
|
version: "1.0.0",
|
|
bin: {
|
|
"echo-args-test": "./index.js",
|
|
},
|
|
}),
|
|
"echo-args-test/index.js": `#!/usr/bin/env node
|
|
console.log(JSON.stringify(process.argv.slice(2)));
|
|
`,
|
|
});
|
|
|
|
// Install to create the .bunx shim in node_modules/.bin
|
|
await using installProc = Bun.spawn({
|
|
cmd: [bunExe(), "install"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stderr: "pipe",
|
|
});
|
|
await installProc.exited;
|
|
|
|
// Verify the .bunx file was created (this is what triggers the fast path)
|
|
const bunxPath = path.join(String(dir), "node_modules", ".bin", "echo-args-test.bunx");
|
|
expect(fs.existsSync(bunxPath)).toBe(true);
|
|
|
|
// Run with an empty string argument - this was triggering the panic
|
|
// We use `bun run` which goes through the same BunXFastPath when .bunx exists
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "run", "echo-args-test", ""],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
// The main assertion is that the process doesn't panic (exit code 3)
|
|
// If the bug is present, this would crash with "reached unreachable code"
|
|
expect(exitCode).not.toBe(3); // panic exit code
|
|
expect(exitCode).toBe(0);
|
|
|
|
// The empty string argument should be passed correctly
|
|
expect(JSON.parse(stdout.trim())).toEqual([""]);
|
|
});
|
|
|
|
test("bunx handles multiple arguments including empty strings", async () => {
|
|
using dir = tempDir("issue-13316-multi", {
|
|
"package.json": JSON.stringify({
|
|
name: "test-project",
|
|
version: "1.0.0",
|
|
dependencies: {
|
|
"echo-args-test": "file:./echo-args-test",
|
|
},
|
|
}),
|
|
"echo-args-test/package.json": JSON.stringify({
|
|
name: "echo-args-test",
|
|
version: "1.0.0",
|
|
bin: {
|
|
"echo-args-test": "./index.js",
|
|
},
|
|
}),
|
|
"echo-args-test/index.js": `#!/usr/bin/env node
|
|
console.log(JSON.stringify(process.argv.slice(2)));
|
|
`,
|
|
});
|
|
|
|
await using installProc = Bun.spawn({
|
|
cmd: [bunExe(), "install"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stderr: "pipe",
|
|
});
|
|
await installProc.exited;
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "run", "echo-args-test", "hello", "", "world"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).not.toBe(3); // panic exit code
|
|
expect(exitCode).toBe(0);
|
|
expect(JSON.parse(stdout.trim())).toEqual(["hello", "", "world"]);
|
|
});
|
|
|
|
// Related to #18275 - bunx concurrently "command with spaces"
|
|
// Arguments containing spaces must be preserved as single arguments
|
|
test("bunx preserves arguments with spaces", async () => {
|
|
using dir = tempDir("issue-13316-spaces", {
|
|
"package.json": JSON.stringify({
|
|
name: "test-project",
|
|
version: "1.0.0",
|
|
dependencies: {
|
|
"echo-args-test": "file:./echo-args-test",
|
|
},
|
|
}),
|
|
"echo-args-test/package.json": JSON.stringify({
|
|
name: "echo-args-test",
|
|
version: "1.0.0",
|
|
bin: {
|
|
"echo-args-test": "./index.js",
|
|
},
|
|
}),
|
|
"echo-args-test/index.js": `#!/usr/bin/env node
|
|
console.log(JSON.stringify(process.argv.slice(2)));
|
|
`,
|
|
});
|
|
|
|
await using installProc = Bun.spawn({
|
|
cmd: [bunExe(), "install"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stderr: "pipe",
|
|
});
|
|
await installProc.exited;
|
|
|
|
// This simulates: bunx concurrently "bun --version"
|
|
// The shell strips the outer quotes, so bunx receives ["concurrently", "bun --version"]
|
|
// This must be preserved as a single argument with spaces
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "run", "echo-args-test", "bun --version", "echo hello world"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(0);
|
|
// Each argument with spaces should be preserved as a single argument
|
|
expect(JSON.parse(stdout.trim())).toEqual(["bun --version", "echo hello world"]);
|
|
});
|
|
});
|