Skip to content
Open
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
11 changes: 9 additions & 2 deletions src/paths/resolve_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1740,8 +1740,15 @@ fn _join_abs_string_buf<'a, const IS_SENTINEL: bool, P: PlatformT>(
let mut part_len: u16 = parts.len() as u16;

while part_i < part_len {
if P::P.is_absolute(parts[part_i as usize]) {
cwd = parts[part_i as usize];
let part = parts[part_i as usize];
// An absolute part resets the join root. This branch produces a
// host-POSIX path, so under `Loose` a Windows drive-letter part
// (`C:/x`) must not become the root: the result would not be
// absolute on this host (e.g. a bare import specifier `C:/`
// joined under `node_modules`). Separator-rooted parts still
// reset; drive-letter parts join as ordinary components.
if P::P.is_absolute(part) && is_sep_any(part[0]) {
cwd = part;
parts = &parts[part_i as usize + 1..];

part_len = parts.len() as u16;
Expand Down
95 changes: 94 additions & 1 deletion test/js/bun/resolve/resolve-error.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import { bunEnv, bunExe, isWindows, tempDir } from "harness";

describe("ResolveMessage", () => {
it("position object does not segfault", async () => {
Expand Down Expand Up @@ -182,3 +182,96 @@ describe.concurrent("long import path overflow", () => {
await run(String(dir), `\`/\${"a/".repeat(300)}x\``);
});
});

// On POSIX, a specifier that merely looks like a Windows absolute path
// ("C:/", "D:\foo") is a bare package specifier. The resolver's loose path
// joins used to adopt it as a new join root, producing a non-absolute path
// that tripped the dirInfoCached assert and crashed the process:
// panic: cannot resolve DirInfo for non-absolute path: C:/
// https://github.com/oven-sh/bun/issues/32016
describe.skipIf(isWindows)("drive letter specifiers on posix", () => {
it.concurrent("fail with a catchable error when searching node_modules", async () => {
using dir = tempDir("resolve-drive-letter", {
"package.json": `{"name": "test", "version": "0.0.0"}`,
// The crash required a node_modules directory in cwd or an ancestor.
// "C:" is an ordinary directory name on posix; Node's CJS loader finds
// files under it, so ours must too (proves the join lands under
// node_modules instead of resetting to a fake root).
"node_modules/C:/real.js": `module.exports = "found";`,
"fixture.mjs": `
const results = [];
for (const specifier of ["C:/", "C:\\\\", "D:\\\\foo", "c:/x"]) {
const entries = {
"import.meta.resolve": () => import.meta.resolve(specifier),
"require.resolve": () => require.resolve(specifier),
"Bun.resolveSync": () => Bun.resolveSync(specifier, import.meta.dir),
};
for (const kind in entries) {
try {
entries[kind]();
results.push(kind + " " + specifier + " resolved");
} catch (e) {
results.push(kind + " " + specifier + " " + e.code);
}
}
try {
await import(specifier);
results.push("import() " + specifier + " resolved");
} catch (e) {
results.push("import() " + specifier + " " + e.code);
}
}
results.push("positive " + require.resolve("C:/real.js").endsWith("/node_modules/C:/real.js"));
console.log(JSON.stringify(results));
`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "fixture.mjs"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual([
...["C:/", "C:\\", "D:\\foo", "c:/x"].flatMap(s => [
`import.meta.resolve ${s} ERR_MODULE_NOT_FOUND`,
`require.resolve ${s} MODULE_NOT_FOUND`,
`Bun.resolveSync ${s} ERR_MODULE_NOT_FOUND`,
`import() ${s} ERR_MODULE_NOT_FOUND`,
]),
"positive true",
]);
expect(exitCode).toBe(0);
});

it.concurrent("fail with a catchable error through the tsconfig baseUrl join", async () => {
using dir = tempDir("resolve-drive-letter-tsconfig", {
"package.json": `{"name": "test", "version": "0.0.0"}`,
"node_modules/.keep": "",
"tsconfig.json": `{"compilerOptions": {"baseUrl": "."}}`,
"fixture.mjs": `
try {
await import("C:/nope");
console.log("resolved");
} catch (e) {
console.log("caught " + e.code);
}
`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "fixture.mjs"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout.trim()).toBe("caught ERR_MODULE_NOT_FOUND");
expect(exitCode).toBe(0);
});
});
Loading