diff --git a/src/install/PackageManager/CommandLineArguments.rs b/src/install/PackageManager/CommandLineArguments.rs index 84b223a973fb..e2b159f25f74 100644 --- a/src/install/PackageManager/CommandLineArguments.rs +++ b/src/install/PackageManager/CommandLineArguments.rs @@ -1295,37 +1295,8 @@ Full documentation is available at https://bun.com/docs/cli/why. cli.concurrent_scripts = strings::parse_int::(concurrency, 10).ok(); } - if let Some(cwd_) = args.option(b"--cwd") { - let mut buf = PathBuffer::uninit(); - let mut buf2 = PathBuffer::uninit(); - - let final_path: &mut bun_core::ZStr = if !cwd_.is_empty() && cwd_[0] == b'.' { - let cwd_len = bun_sys::getcwd(&mut buf[..])?; - let cwd = &buf[..cwd_len]; - let parts: [&[u8]; 1] = [cwd_]; - let len = Path::resolve_path::join_abs_string_buf::( - cwd, - &mut buf2[..], - &parts, - ) - .len(); - buf2[len] = 0; - bun_core::ZStr::from_buf_mut(&mut buf2[..], len) - } else { - buf[..cwd_.len()].copy_from_slice(cwd_); - buf[cwd_.len()] = 0; - bun_core::ZStr::from_buf_mut(&mut buf[..], cwd_.len()) - }; - if let Err(err) = bun_sys::chdir(final_path) { - Output::err_generic( - "failed to change directory to \"{}\": {}\n", - ( - bstr::BStr::new(final_path.as_bytes()), - bstr::BStr::new(err.name()), - ), - ); - Global::crash(); - } + if let Some(cwd) = args.option(b"--cwd") { + change_directory(cwd)?; } if subcommand == Subcommand::Update { @@ -1449,3 +1420,48 @@ Full documentation is available at https://bun.com/docs/cli/why. Ok(cli) } } + +/// `--cwd`. Exits the process when the directory cannot be entered. +fn change_directory(arg: &[u8]) -> Result<(), crate::Error> { + let mut buf = PathBuffer::uninit(); + let mut buf2 = PathBuffer::uninit(); + let too_long = || bun_sys::Error::from_code(bun_sys::E::ENAMETOOLONG, bun_sys::Tag::chdir); + + // Either buffer keeps its last byte for the NUL terminator `chdir` needs. + let resolved: Result<&bun_core::ZStr, bun_sys::Error> = if arg.first() == Some(&b'.') { + let cwd_len = bun_sys::getcwd(&mut buf[..])?; + let out_len = buf2.len() - 1; + match Path::resolve_path::join_abs_string_buf_checked::( + &buf[..cwd_len], + &mut buf2[..out_len], + &[arg], + ) + .map(|joined| joined.len()) + { + Some(len) => { + buf2[len] = 0; + Ok(bun_core::ZStr::from_buf(&buf2[..], len)) + } + None => Err(too_long()), + } + } else if arg.len() < buf.len() { + buf[..arg.len()].copy_from_slice(arg); + buf[arg.len()] = 0; + Ok(bun_core::ZStr::from_buf(&buf[..], arg.len())) + } else { + Err(too_long()) + }; + + let (path, err) = match resolved { + Ok(path) => match bun_sys::chdir(path) { + Ok(()) => return Ok(()), + Err(err) => (path.as_bytes(), err), + }, + Err(err) => (arg, err), + }; + Output::err_generic( + "failed to change directory to \"{}\": {}\n", + (bstr::BStr::new(path), bstr::BStr::new(err.name())), + ); + Global::crash(); +} diff --git a/test/cli/install/bun-install.test.ts b/test/cli/install/bun-install.test.ts index e35753d1be20..74cc0c65677f 100644 --- a/test/cli/install/bun-install.test.ts +++ b/test/cli/install/bun-install.test.ts @@ -6,6 +6,8 @@ import { bunEnv, bunExe, bunEnv as env, + isAndroid, + isLinux, isWindows, joinP, readdirSorted, @@ -10296,3 +10298,36 @@ it.each([ expect(exitCode).not.toBe(0); }); }); + +// `--cwd` is staged in a PATH_MAX-sized buffer (4096 bytes on Linux and Android, 1024 on macOS and +// the BSDs) before chdir. Values that did not leave room for the NUL terminator used to abort the +// process; the buffer on Windows (~96 KiB) is larger than any command line, so only POSIX is affected. +describe.concurrent.skipIf(isWindows)("--cwd that does not fit the path buffer", () => { + const PATH_MAX = isLinux || isAndroid ? 4096 : 1024; + const name = (length: number) => Buffer.alloc(length, "a").toString(); + + it.each([ + ["install, PATH_MAX - 1 bytes (rejected by the kernel)", "install", name(PATH_MAX - 1)], + ["install, exactly PATH_MAX bytes", "install", name(PATH_MAX)], + ["install, longer than PATH_MAX", "install", name(PATH_MAX + 1000)], + ["install, ./ prefix longer than PATH_MAX", "install", "./" + name(PATH_MAX + 1000)], + ["add, longer than PATH_MAX", "add", name(PATH_MAX + 1000)], + ])("%s", async (_, subcommand, cwd) => { + using dir = tempDir("install-cwd-too-long", { + "package.json": JSON.stringify({ name: "foo", version: "0.0.1" }), + }); + + await using proc = spawn({ + cmd: [bunExe(), subcommand, "--cwd", cwd, ...(subcommand === "add" ? ["bar"] : [])], + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + env, + }); + const [out, err, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(err).toContain(`failed to change directory to "${cwd}": ENAMETOOLONG`); + expect(out).toBe(""); + expect(exitCode).toBe(1); + }); +});