From 4e44ec1ba3eda63b419b46089bb458ab1b79096a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Thu, 6 Aug 2026 04:36:25 +0800 Subject: [PATCH 1/3] fix(command): resolve relative PATH entries against cwd --- .../package.json | 6 + .../packages/app/package.json | 3 + .../command_exec_relative_path_cwd/setup.js | 9 ++ .../snapshots.toml | 9 ++ .../command_exec_relative_path_cwd.md | 14 +++ crates/vp_command/src/lib.rs | 108 +++++++++++++++++- 6 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/packages/app/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/setup.js create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/snapshots.toml create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/snapshots/command_exec_relative_path_cwd.md diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/package.json new file mode 100644 index 0000000000..f74441e9ab --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/package.json @@ -0,0 +1,6 @@ +{ + "name": "exec-relative-path-cwd", + "workspaces": [ + "packages/*" + ] +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/packages/app/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/packages/app/package.json new file mode 100644 index 0000000000..fd1f8f6386 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/packages/app/package.json @@ -0,0 +1,3 @@ +{ + "name": "app" +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/setup.js b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/setup.js new file mode 100644 index 0000000000..ee06996a73 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/setup.js @@ -0,0 +1,9 @@ +const fs = require('fs'); + +fs.mkdirSync('packages/app/tools', { recursive: true }); +fs.writeFileSync( + 'packages/app/tools/fake-node', + '#!/usr/bin/env node\nconsole.log("resolved from package cwd");\n', + { mode: 0o755 }, +); +fs.writeFileSync('packages/app/tools/fake-node.cmd', '@node "%~dp0\\fake-node" %*\n'); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/snapshots.toml new file mode 100644 index 0000000000..831192feb1 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/snapshots.toml @@ -0,0 +1,9 @@ +[[case]] +name = "command_exec_relative_path_cwd" +vp = "local" +skip-platforms = ["windows"] +comment = "A relative PATH entry must resolve against the selected package cwd, not the vp process cwd." +steps = [ + { argv = ["node", "setup.js"], snapshot = false, continue-on-failure = true }, + { argv = ["vp", "exec", "--filter", "app", "--", "fake-node"], envs = [["PATH", "./tools:${PATH}"]], comment = "relative PATH entry resolves from the selected package", continue-on-failure = true }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/snapshots/command_exec_relative_path_cwd.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/snapshots/command_exec_relative_path_cwd.md new file mode 100644 index 0000000000..4dc3f5b257 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/snapshots/command_exec_relative_path_cwd.md @@ -0,0 +1,14 @@ +# command_exec_relative_path_cwd + +A relative PATH entry must resolve against the selected package cwd, not the vp process cwd. + +## `node setup.js` + + +## `PATH=./tools:${PATH} vp exec --filter app -- fake-node` + +relative PATH entry resolves from the selected package + +``` +resolved from package cwd +``` diff --git a/crates/vp_command/src/lib.rs b/crates/vp_command/src/lib.rs index d388688756..045dadd41a 100644 --- a/crates/vp_command/src/lib.rs +++ b/crates/vp_command/src/lib.rs @@ -21,6 +21,15 @@ use vt_path::{AbsolutePath, AbsolutePathBuf, RelativePathBuf}; mod ps1_shim; +fn normalize_path_env( + path_env: &OsStr, + cwd: &AbsolutePath, +) -> Result { + std::env::join_paths(std::env::split_paths(path_env).map(|path| { + if path.is_absolute() || path.starts_with("~") { path } else { cwd.as_path().join(path) } + })) +} + /// Result of running a command with fspy tracking. #[derive(Debug)] pub struct FspyCommandResult { @@ -39,6 +48,7 @@ pub fn resolve_bin( path_env: Option<&OsStr>, cwd: impl AsRef, ) -> Result { + let cwd = cwd.as_ref(); let current_path; let path_env = if let Some(p) = path_env { p @@ -46,8 +56,14 @@ pub fn resolve_bin( current_path = std::env::var_os("PATH").unwrap_or_default(); ¤t_path }; - let path = which::which_in(bin_name, Some(path_env), cwd.as_ref()) + // `which` resolves relative PATH entries against the process cwd instead of the supplied + // command cwd. Commands are spawned with `cwd`, so resolve the entries the same way first; + // leave `~` entries for `which` to expand against the user's home directory. + let path_env = normalize_path_env(path_env, cwd) .map_err(|_| Error::CannotFindBinaryPath(bin_name.into()))?; + let path = which::which_in(bin_name, Some(&path_env), cwd) + .map_err(|_| Error::CannotFindBinaryPath(bin_name.into()))?; + let path = if path.is_absolute() { path } else { cwd.as_path().join(path) }; AbsolutePathBuf::new(path).ok_or_else(|| Error::CannotFindBinaryPath(bin_name.into())) } @@ -399,6 +415,96 @@ mod tests { tempdir().expect("Failed to create temp directory") } + #[cfg(unix)] + fn create_executable(path: &std::path::Path) { + use std::{fs, os::unix::fs::PermissionsExt}; + + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(path, "#!/bin/sh\nexit 0\n").unwrap(); + let mut permissions = fs::metadata(path).unwrap().permissions(); + permissions.set_mode(0o755); + fs::set_permissions(path, permissions).unwrap(); + } + + #[cfg(unix)] + #[test] + fn test_resolve_bin_with_relative_path_entry() { + use std::path::PathBuf; + + let temp_dir = create_temp_dir(); + let cwd_path = temp_dir.path().canonicalize().unwrap(); + let cwd = AbsolutePathBuf::new(cwd_path.clone()).unwrap(); + let bin_dir = cwd_path.join("node_modules/.bin"); + let bin_path = bin_dir.join("fake-node"); + let fallback_bin_dir = cwd_path.join("fallback-bin"); + let fallback_bin_path = fallback_bin_dir.join("fake-node"); + + create_executable(&bin_path); + create_executable(&fallback_bin_path); + + let path_env = + std::env::join_paths([PathBuf::from("./node_modules/.bin"), fallback_bin_dir]).unwrap(); + let resolved = resolve_bin("fake-node", Some(&path_env), &cwd).unwrap(); + + assert_eq!(resolved.into_path_buf(), bin_path); + } + + #[cfg(unix)] + #[test] + fn test_resolve_bin_continues_after_missing_relative_path_entry() { + use std::path::PathBuf; + + let temp_dir = create_temp_dir(); + let cwd_path = temp_dir.path().canonicalize().unwrap(); + let cwd = AbsolutePathBuf::new(cwd_path.clone()).unwrap(); + let fallback_bin_dir = cwd_path.join("fallback-bin"); + let fallback_bin_path = fallback_bin_dir.join("fake-node"); + + create_executable(&fallback_bin_path); + + let path_env = + std::env::join_paths([PathBuf::from("./missing-bin"), fallback_bin_dir]).unwrap(); + let resolved = resolve_bin("fake-node", Some(&path_env), &cwd).unwrap(); + + assert_eq!(resolved.into_path_buf(), fallback_bin_path); + } + + #[cfg(unix)] + #[test] + fn test_resolve_bin_with_empty_path_entry() { + use std::path::PathBuf; + + let temp_dir = create_temp_dir(); + let cwd_path = temp_dir.path().canonicalize().unwrap(); + let cwd = AbsolutePathBuf::new(cwd_path.clone()).unwrap(); + let bin_path = cwd_path.join("fake-node"); + + create_executable(&bin_path); + + let path_env = std::env::join_paths([PathBuf::new()]).unwrap(); + let resolved = resolve_bin("fake-node", Some(&path_env), &cwd).unwrap(); + + assert_eq!(resolved.into_path_buf(), bin_path); + } + + #[cfg(unix)] + #[test] + fn test_normalize_path_env_preserves_tilde_entry() { + use std::path::PathBuf; + + let temp_dir = create_temp_dir(); + let cwd_path = temp_dir.path().canonicalize().unwrap(); + let cwd = AbsolutePathBuf::new(cwd_path).unwrap(); + let path_env = std::env::join_paths([PathBuf::from("~/bin")]).unwrap(); + + let normalized = normalize_path_env(&path_env, &cwd).unwrap(); + + assert_eq!( + std::env::split_paths(&normalized).collect::>(), + [PathBuf::from("~/bin")] + ); + } + mod run_command_tests { use super::*; From c2d083d2318e67c75da4e294fd82432b4ccba9e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Mon, 10 Aug 2026 23:27:05 +0800 Subject: [PATCH 2/3] test(command): cover relative PATH variants --- .../command_exec_relative_path_cwd/setup.js | 19 ++- .../snapshots.toml | 2 + .../command_exec_relative_path_cwd.md | 16 +++ crates/vp_command/src/lib.rs | 120 +++++++++++++++++- 4 files changed, 148 insertions(+), 9 deletions(-) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/setup.js b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/setup.js index ee06996a73..331546320a 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/setup.js +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/setup.js @@ -1,9 +1,14 @@ const fs = require('fs'); -fs.mkdirSync('packages/app/tools', { recursive: true }); -fs.writeFileSync( - 'packages/app/tools/fake-node', - '#!/usr/bin/env node\nconsole.log("resolved from package cwd");\n', - { mode: 0o755 }, -); -fs.writeFileSync('packages/app/tools/fake-node.cmd', '@node "%~dp0\\fake-node" %*\n'); +function writeFakeNode(directory, message) { + fs.mkdirSync(directory, { recursive: true }); + fs.writeFileSync( + `${directory}/fake-node`, + `#!/usr/bin/env node\nconsole.log(${JSON.stringify(message)});\n`, + { mode: 0o755 }, + ); + fs.writeFileSync(`${directory}/fake-node.cmd`, '@node "%~dp0\\fake-node" %*\n'); +} + +writeFakeNode('packages/app/tools', 'resolved from package cwd'); +writeFakeNode('packages/shared-tools', 'resolved from parent relative PATH'); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/snapshots.toml index 831192feb1..58d9222323 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/snapshots.toml @@ -6,4 +6,6 @@ comment = "A relative PATH entry must resolve against the selected package cwd, steps = [ { argv = ["node", "setup.js"], snapshot = false, continue-on-failure = true }, { argv = ["vp", "exec", "--filter", "app", "--", "fake-node"], envs = [["PATH", "./tools:${PATH}"]], comment = "relative PATH entry resolves from the selected package", continue-on-failure = true }, + { argv = ["vp", "exec", "--filter", "app", "--", "fake-node"], envs = [["PATH", "tools:${PATH}"]], comment = "plain relative PATH entry resolves from the selected package", continue-on-failure = true }, + { argv = ["vp", "exec", "--filter", "app", "--", "fake-node"], envs = [["PATH", "../shared-tools:${PATH}"]], comment = "parent relative PATH entry resolves from the selected package", continue-on-failure = true }, ] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/snapshots/command_exec_relative_path_cwd.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/snapshots/command_exec_relative_path_cwd.md index 4dc3f5b257..cfb4bb529d 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/snapshots/command_exec_relative_path_cwd.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_exec_relative_path_cwd/snapshots/command_exec_relative_path_cwd.md @@ -12,3 +12,19 @@ relative PATH entry resolves from the selected package ``` resolved from package cwd ``` + +## `PATH=tools:${PATH} vp exec --filter app -- fake-node` + +plain relative PATH entry resolves from the selected package + +``` +resolved from package cwd +``` + +## `PATH=../shared-tools:${PATH} vp exec --filter app -- fake-node` + +parent relative PATH entry resolves from the selected package + +``` +resolved from parent relative PATH +``` diff --git a/crates/vp_command/src/lib.rs b/crates/vp_command/src/lib.rs index 045dadd41a..76b8ada3f1 100644 --- a/crates/vp_command/src/lib.rs +++ b/crates/vp_command/src/lib.rs @@ -10,6 +10,7 @@ use std::os::fd::{BorrowedFd, RawFd}; use std::{ collections::HashMap, ffi::{OsStr, OsString}, + path::Path, process::{ExitStatus, Stdio}, }; @@ -26,10 +27,39 @@ fn normalize_path_env( cwd: &AbsolutePath, ) -> Result { std::env::join_paths(std::env::split_paths(path_env).map(|path| { - if path.is_absolute() || path.starts_with("~") { path } else { cwd.as_path().join(path) } + if path.starts_with("~") || !is_plain_relative_path(&path) { + path + } else { + cwd.as_path().join(path) + } })) } +/// Return whether a PATH entry is an ordinary relative path that should be resolved against the +/// command cwd. This includes `tools`, `./tools`, and `../tools`. +/// +/// Windows drive-relative (`C:tools`) and root-relative (`\tools`) paths have distinct native +/// semantics. They are intentionally left unchanged, as are absolute drive and UNC paths. +fn is_plain_relative_path(path: &Path) -> bool { + #[cfg(windows)] + { + !path.has_root() + && path.components().next().is_some_and(|component| { + matches!( + component, + std::path::Component::CurDir + | std::path::Component::ParentDir + | std::path::Component::Normal(_) + ) + }) + } + + #[cfg(not(windows))] + { + !path.is_absolute() + } +} + /// Result of running a command with fspy tracking. #[derive(Debug)] pub struct FspyCommandResult { @@ -63,7 +93,7 @@ pub fn resolve_bin( .map_err(|_| Error::CannotFindBinaryPath(bin_name.into()))?; let path = which::which_in(bin_name, Some(&path_env), cwd) .map_err(|_| Error::CannotFindBinaryPath(bin_name.into()))?; - let path = if path.is_absolute() { path } else { cwd.as_path().join(path) }; + let path = if is_plain_relative_path(&path) { cwd.as_path().join(path) } else { path }; AbsolutePathBuf::new(path).ok_or_else(|| Error::CannotFindBinaryPath(bin_name.into())) } @@ -505,6 +535,92 @@ mod tests { ); } + #[test] + fn test_normalize_path_env_resolves_plain_relative_entry_against_cwd() { + use std::path::PathBuf; + + let temp_dir = create_temp_dir(); + let cwd_path = temp_dir.path().canonicalize().unwrap(); + let cwd = AbsolutePathBuf::new(cwd_path.clone()).unwrap(); + let path_env = std::env::join_paths([PathBuf::from("node_modules/.bin")]).unwrap(); + + let normalized = normalize_path_env(&path_env, &cwd).unwrap(); + + assert_eq!( + std::env::split_paths(&normalized).collect::>(), + [cwd_path.join("node_modules/.bin")] + ); + } + + #[test] + fn test_normalize_path_env_resolves_common_relative_entries_and_keeps_absolute_entries() { + use std::path::PathBuf; + + let temp_dir = create_temp_dir(); + let cwd_path = temp_dir.path().canonicalize().unwrap(); + let cwd = AbsolutePathBuf::new(cwd_path.clone()).unwrap(); + let absolute_entry = cwd_path.join("absolute-bin"); + let path_env = std::env::join_paths([ + PathBuf::from("node_modules/.bin"), + PathBuf::from("./tools"), + PathBuf::from("../shared-bin"), + absolute_entry.clone(), + ]) + .unwrap(); + + let normalized = normalize_path_env(&path_env, &cwd).unwrap(); + + assert_eq!( + std::env::split_paths(&normalized).collect::>(), + [ + cwd_path.join("node_modules/.bin"), + cwd_path.join("./tools"), + cwd_path.join("../shared-bin"), + absolute_entry, + ] + ); + } + + #[cfg(windows)] + #[test] + fn test_normalize_path_env_preserves_windows_absolute_entries() { + use std::path::PathBuf; + + let temp_dir = create_temp_dir(); + let cwd_path = temp_dir.path().canonicalize().unwrap(); + let cwd = AbsolutePathBuf::new(cwd_path).unwrap(); + + for entry in [r"C:\tools\bin", r"\\server\share\bin"] { + let path_env = std::env::join_paths([PathBuf::from(entry)]).unwrap(); + let normalized = normalize_path_env(&path_env, &cwd).unwrap(); + + assert_eq!( + std::env::split_paths(&normalized).collect::>(), + [PathBuf::from(entry)] + ); + } + } + + #[cfg(windows)] + #[test] + fn test_normalize_path_env_does_not_rewrite_windows_special_relative_entries() { + use std::path::PathBuf; + + let temp_dir = create_temp_dir(); + let cwd_path = temp_dir.path().canonicalize().unwrap(); + let cwd = AbsolutePathBuf::new(cwd_path).unwrap(); + + for entry in [r"C:tools\bin", r"\tools\bin"] { + let path_env = std::env::join_paths([PathBuf::from(entry)]).unwrap(); + let normalized = normalize_path_env(&path_env, &cwd).unwrap(); + + assert_eq!( + std::env::split_paths(&normalized).collect::>(), + [PathBuf::from(entry)] + ); + } + } + mod run_command_tests { use super::*; From fb1cd60261aadfaa870a9bc5720d11e3f6d57b50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Tue, 11 Aug 2026 16:04:31 +0800 Subject: [PATCH 3/3] fix(command): resolve relative PATH entries safely --- crates/vp_command/src/lib.rs | 160 ++++++++++++++++------------------- 1 file changed, 72 insertions(+), 88 deletions(-) diff --git a/crates/vp_command/src/lib.rs b/crates/vp_command/src/lib.rs index 1a15988de3..a8bcd09d72 100644 --- a/crates/vp_command/src/lib.rs +++ b/crates/vp_command/src/lib.rs @@ -22,19 +22,6 @@ use vt_path::{AbsolutePath, AbsolutePathBuf, RelativePathBuf}; mod ps1_shim; -fn normalize_path_env( - path_env: &OsStr, - cwd: &AbsolutePath, -) -> Result { - std::env::join_paths(std::env::split_paths(path_env).map(|path| { - if path.starts_with("~") || !is_plain_relative_path(&path) { - path - } else { - cwd.as_path().join(path) - } - })) -} - /// Return whether a PATH entry is an ordinary relative path that should be resolved against the /// command cwd. This includes `tools`, `./tools`, and `../tools`. /// @@ -60,6 +47,25 @@ fn is_plain_relative_path(path: &Path) -> bool { } } +fn resolve_bin_from_path_entry( + bin_name: &str, + path_entry: &Path, + cwd: &AbsolutePath, +) -> Option { + if path_entry.starts_with("~") || !is_plain_relative_path(path_entry) { + // Preserve tilde expansion and Windows-special path semantics by passing the original + // entry through `which`. Since this entry came from `split_paths`, serializing it alone + // cannot introduce the command cwd's PATH separator. + let path_env = std::env::join_paths([path_entry]).ok()?; + which::which_in(bin_name, Some(path_env), cwd).ok() + } else { + // Search the absolute candidate directly. Re-serializing `cwd.join(path_entry)` into PATH + // would fail on Unix when cwd contains `:`, even though the relative PATH entry is valid. + let candidate = cwd.as_path().join(path_entry).join(bin_name); + which::which_in(candidate, None::<&OsStr>, cwd).ok() + } +} + /// Result of running a command with fspy tracking. #[derive(Debug)] pub struct FspyCommandResult { @@ -86,13 +92,20 @@ pub fn resolve_bin( current_path = std::env::var_os("PATH").unwrap_or_default(); ¤t_path }; - // `which` resolves relative PATH entries against the process cwd instead of the supplied - // command cwd. Commands are spawned with `cwd`, so resolve the entries the same way first; - // leave `~` entries for `which` to expand against the user's home directory. - let path_env = normalize_path_env(path_env, cwd) - .map_err(|_| Error::CannotFindBinaryPath(bin_name.into()))?; - let path = which::which_in(bin_name, Some(&path_env), cwd) - .map_err(|_| Error::CannotFindBinaryPath(bin_name.into()))?; + let bin_path = Path::new(bin_name); + let path = if bin_path.is_absolute() || bin_path.components().count() > 1 { + // Preserve `which` semantics for an explicit program path: it is resolved directly against + // `cwd` and does not search PATH. + which::which_in(bin_name, Some(path_env), cwd) + .map_err(|_| Error::CannotFindBinaryPath(bin_name.into()))? + } else { + // `which` resolves relative PATH entries against the process cwd instead of the supplied + // command cwd. Search each entry in order so ordinary relative entries can be resolved + // against `cwd` without serializing that absolute path back into PATH. + std::env::split_paths(path_env) + .find_map(|entry| resolve_bin_from_path_entry(bin_name, &entry, cwd)) + .ok_or_else(|| Error::CannotFindBinaryPath(bin_name.into()))? + }; let path = if is_plain_relative_path(&path) { cwd.as_path().join(path) } else { path }; AbsolutePathBuf::new(path).ok_or_else(|| Error::CannotFindBinaryPath(bin_name.into())) } @@ -519,105 +532,76 @@ mod tests { #[cfg(unix)] #[test] - fn test_normalize_path_env_preserves_tilde_entry() { + fn test_resolve_bin_with_relative_path_entry_when_cwd_contains_path_separator() { use std::path::PathBuf; let temp_dir = create_temp_dir(); - let cwd_path = temp_dir.path().canonicalize().unwrap(); - let cwd = AbsolutePathBuf::new(cwd_path).unwrap(); - let path_env = std::env::join_paths([PathBuf::from("~/bin")]).unwrap(); + let cwd_path = temp_dir.path().join("project:fixture"); + std::fs::create_dir_all(&cwd_path).unwrap(); + let cwd_path = cwd_path.canonicalize().unwrap(); + let cwd = AbsolutePathBuf::new(cwd_path.clone()).unwrap(); + let bin_path = cwd_path.join("tools/fake-node"); + create_executable(&bin_path); + let path_env = std::env::join_paths([PathBuf::from("./tools")]).unwrap(); - let normalized = normalize_path_env(&path_env, &cwd).unwrap(); + let resolved = resolve_bin("fake-node", Some(&path_env), &cwd).unwrap(); - assert_eq!( - std::env::split_paths(&normalized).collect::>(), - [PathBuf::from("~/bin")] - ); + assert_eq!(resolved.into_path_buf(), bin_path); } + #[cfg(unix)] #[test] - fn test_normalize_path_env_resolves_plain_relative_entry_against_cwd() { + fn test_resolve_bin_continues_after_relative_entry_when_cwd_contains_path_separator() { use std::path::PathBuf; let temp_dir = create_temp_dir(); - let cwd_path = temp_dir.path().canonicalize().unwrap(); - let cwd = AbsolutePathBuf::new(cwd_path.clone()).unwrap(); - let path_env = std::env::join_paths([PathBuf::from("node_modules/.bin")]).unwrap(); + let cwd_path = temp_dir.path().join("project:fixture"); + std::fs::create_dir_all(&cwd_path).unwrap(); + let cwd_path = cwd_path.canonicalize().unwrap(); + let cwd = AbsolutePathBuf::new(cwd_path).unwrap(); + let fallback_bin_dir = temp_dir.path().join("fallback-bin"); + let fallback_bin_path = fallback_bin_dir.join("fake-node"); + create_executable(&fallback_bin_path); + let path_env = + std::env::join_paths([PathBuf::from("./missing-bin"), fallback_bin_dir]).unwrap(); - let normalized = normalize_path_env(&path_env, &cwd).unwrap(); + let resolved = resolve_bin("fake-node", Some(&path_env), &cwd).unwrap(); - assert_eq!( - std::env::split_paths(&normalized).collect::>(), - [cwd_path.join("node_modules/.bin")] - ); + assert_eq!(resolved.into_path_buf(), fallback_bin_path); } + #[cfg(unix)] #[test] - fn test_normalize_path_env_resolves_common_relative_entries_and_keeps_absolute_entries() { + fn test_resolve_bin_with_explicit_relative_program_path() { use std::path::PathBuf; let temp_dir = create_temp_dir(); - let cwd_path = temp_dir.path().canonicalize().unwrap(); + let cwd_path = temp_dir.path().join("project:fixture"); + std::fs::create_dir_all(&cwd_path).unwrap(); + let cwd_path = cwd_path.canonicalize().unwrap(); let cwd = AbsolutePathBuf::new(cwd_path.clone()).unwrap(); - let absolute_entry = cwd_path.join("absolute-bin"); - let path_env = std::env::join_paths([ - PathBuf::from("node_modules/.bin"), - PathBuf::from("./tools"), - PathBuf::from("../shared-bin"), - absolute_entry.clone(), - ]) - .unwrap(); - - let normalized = normalize_path_env(&path_env, &cwd).unwrap(); - - assert_eq!( - std::env::split_paths(&normalized).collect::>(), - [ - cwd_path.join("node_modules/.bin"), - cwd_path.join("./tools"), - cwd_path.join("../shared-bin"), - absolute_entry, - ] - ); + let bin_path = cwd_path.join("scripts/fake-node"); + create_executable(&bin_path); + let path_env = std::env::join_paths([PathBuf::from("./tools")]).unwrap(); + + let resolved = resolve_bin("./scripts/fake-node", Some(&path_env), &cwd).unwrap(); + + assert_eq!(resolved.into_path_buf(), bin_path); } #[cfg(windows)] #[test] - fn test_normalize_path_env_preserves_windows_absolute_entries() { - use std::path::PathBuf; - - let temp_dir = create_temp_dir(); - let cwd_path = temp_dir.path().canonicalize().unwrap(); - let cwd = AbsolutePathBuf::new(cwd_path).unwrap(); - + fn test_windows_absolute_entries_are_not_plain_relative_paths() { for entry in [r"C:\tools\bin", r"\\server\share\bin"] { - let path_env = std::env::join_paths([PathBuf::from(entry)]).unwrap(); - let normalized = normalize_path_env(&path_env, &cwd).unwrap(); - - assert_eq!( - std::env::split_paths(&normalized).collect::>(), - [PathBuf::from(entry)] - ); + assert!(!is_plain_relative_path(Path::new(entry))); } } #[cfg(windows)] #[test] - fn test_normalize_path_env_does_not_rewrite_windows_special_relative_entries() { - use std::path::PathBuf; - - let temp_dir = create_temp_dir(); - let cwd_path = temp_dir.path().canonicalize().unwrap(); - let cwd = AbsolutePathBuf::new(cwd_path).unwrap(); - + fn test_windows_special_relative_entries_are_not_plain_relative_paths() { for entry in [r"C:tools\bin", r"\tools\bin"] { - let path_env = std::env::join_paths([PathBuf::from(entry)]).unwrap(); - let normalized = normalize_path_env(&path_env, &cwd).unwrap(); - - assert_eq!( - std::env::split_paths(&normalized).collect::>(), - [PathBuf::from(entry)] - ); + assert!(!is_plain_relative_path(Path::new(entry))); } }