Fix Windows path issues with tarball URLs containing query parameters

Fixes #20647

- Strip query parameters from tarball names when creating temp directories on Windows
- Add Windows-specific handling to remove slashes from URL paths
- Prevent "BadPathName" and "ENOTEMPTY" errors when installing tarballs with query params

Added comprehensive tests for installing tarballs with query parameters to ensure they install successfully on Windows.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Zack Radisic
2025-09-06 18:18:31 -07:00
committed by Claude Bot
parent 7335cb747b
commit 56bd81fdc2
3 changed files with 69 additions and 3 deletions

View File

@@ -764,6 +764,7 @@ const Output = bun.Output;
const Path = bun.path;
const Progress = bun.Progress;
const default_allocator = bun.default_allocator;
const strings = bun.strings;
const Command = bun.cli.Command;
const File = bun.sys.File;

View File

@@ -121,6 +121,12 @@ fn extract(this: *const ExtractTarball, log: *logger.Log, tgz_bytes: []const u8)
const basename = brk: {
var tmp = name;
// Strip query parameters from the name (e.g., "?token=abc" from "package.tgz?token=abc")
// This is essential on Windows where '?' is an invalid path character
if (strings.indexOfChar(tmp, '?')) |query_index| {
tmp = tmp[0..query_index];
}
// Handle URLs - extract just the filename from the URL
if (strings.hasPrefixComptime(tmp, "https://") or strings.hasPrefixComptime(tmp, "http://")) {
tmp = std.fs.path.basename(tmp);
@@ -130,15 +136,20 @@ fn extract(this: *const ExtractTarball, log: *logger.Log, tgz_bytes: []const u8)
} else if (strings.endsWithComptime(tmp, ".tar.gz")) {
tmp = tmp[0 .. tmp.len - 7];
}
} else if (tmp[0] == '@') {
} else if (tmp.len > 0 and tmp[0] == '@') {
if (strings.indexOfChar(tmp, '/')) |i| {
tmp = tmp[i + 1 ..];
if (tmp.len > i + 1) {
tmp = tmp[i + 1 ..];
}
}
}
if (comptime Environment.isWindows) {
// On Windows, colons are invalid in paths (except for drive letters)
// URLs like "https://example.com/package.tgz" would have a colon
if (strings.lastIndexOfChar(tmp, ':')) |i| {
tmp = tmp[i + 1 ..];
if (i > "C:".len)
tmp = tmp[i + 1 ..];
}
}

View File

@@ -2428,3 +2428,57 @@ it("should install tarball with tarball dependencies", async () => {
await access(join(add_dir, "node_modules", "test-parent"));
await access(join(add_dir, "node_modules", "test-child"));
});
it("should install tarball with query parameters", async () => {
// Regression test for issue #20647
// Previously on Windows, tarball URLs with query parameters would fail with BadPathName errors
// Use a local server to serve the tarball
using server = Bun.serve({
port: 0,
fetch(req) {
// Serve the same tarball regardless of query parameters
return new Response(Bun.file(join(__dirname, "baz-0.0.3.tgz")));
},
});
const server_url = server.url.href.replace(/\/+$/, "");
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "foo",
version: "0.0.1",
}),
);
// Add a tarball with query parameters (used for auth tokens, cache busting, etc.)
const tarballUrl = `${server_url}/package.tgz?token=abc123&timestamp=2024`;
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "add", tarballUrl],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err = await stderr.text();
expect(err).toContain("Saved lockfile");
const out = await stdout.text();
expect(out).toContain("installed baz@");
expect(await exited).toBe(0);
// Verify the package was actually installed
expect(await readdirSorted(join(package_dir, "node_modules"))).toContain("baz");
expect(await file(join(package_dir, "node_modules", "baz", "package.json")).json()).toEqual({
name: "baz",
version: "0.0.3",
bin: {
"baz-run": "index.js",
},
});
// Verify package.json has the dependency with the full URL including query params
const pkg = await file(join(package_dir, "package.json")).json();
expect(pkg.dependencies["baz"]).toBe(tarballUrl);
});