From 86acfaa00821800aa143dd29eaf553628d559cc6 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Fri, 5 Jun 2026 23:52:19 +0000 Subject: [PATCH 1/5] security: prevent http install path escape --- src/backend/http.rs | 61 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/src/backend/http.rs b/src/backend/http.rs index 4e4e972888..d2a7095eaf 100644 --- a/src/backend/http.rs +++ b/src/backend/http.rs @@ -506,6 +506,14 @@ impl HttpBackend { // Symlink creation // ------------------------------------------------------------------------- + fn install_version_name(tv: &ToolVersion, cache_key: &str) -> String { + if tv.version == "latest" || tv.version.is_empty() { + cache_key[..7.min(cache_key.len())].to_string() + } else { + tv.tv_pathname() + } + } + /// Create install symlink(s) from install directory to cache fn create_install_symlink( &self, @@ -517,13 +525,8 @@ impl HttpBackend { let cache_path = self.cache_path(cache_key); // Determine version name for install path - let version_name = if tv.version == "latest" || tv.version.is_empty() { - &cache_key[..7.min(cache_key.len())] // Content-based versioning - } else { - &tv.version - }; - - let install_path = tv.ba().installs_path.join(version_name); + let version_name = Self::install_version_name(tv, cache_key); + let install_path = tv.ba().installs_path.join(&version_name); // Clean up existing install if install_path.exists() { @@ -642,6 +645,50 @@ impl HttpBackend { } } +#[cfg(test)] +mod tests { + use super::*; + use crate::cli::args::BackendResolution; + use crate::toolset::{ToolRequest, ToolSource}; + + fn http_test_tv(version: &str) -> ToolVersion { + let backend = Arc::new(BackendArg::new_raw( + "http-absolute-version".to_string(), + Some("http:absolute-version".to_string()), + "absolute-version".to_string(), + None, + BackendResolution::new(true), + )); + let request = ToolRequest::Version { + backend, + version: version.to_string(), + options: ToolVersionOptions::default(), + source: ToolSource::Argument, + }; + ToolVersion::new(request, version.to_string()) + } + + #[test] + fn install_symlink_path_uses_sanitized_version_pathname() { + let tv = http_test_tv("/outside-root/mise-http-version-out/selected-prefix"); + let version_name = HttpBackend::install_version_name(&tv, "abcdef123456"); + + assert_eq!( + version_name, + "-outside-root-mise-http-version-out-selected-prefix" + ); + assert!(!Path::new(&version_name).is_absolute()); + } + + #[test] + fn latest_install_symlink_still_uses_content_version() { + let tv = http_test_tv("latest"); + let version_name = HttpBackend::install_version_name(&tv, "abcdef123456"); + + assert_eq!(version_name, "abcdef1"); + } +} + /// Returns install-time-only option keys for HTTP backend. pub fn install_time_option_keys() -> Vec { vec![ From a33c831dfc65bf21fcba39f766e568c20c66802d Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Sat, 6 Jun 2026 00:01:16 +0000 Subject: [PATCH 2/5] security: harden http install version names --- src/backend/http.rs | 71 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 6 deletions(-) diff --git a/src/backend/http.rs b/src/backend/http.rs index d2a7095eaf..bb925a63f5 100644 --- a/src/backend/http.rs +++ b/src/backend/http.rs @@ -508,9 +508,26 @@ impl HttpBackend { fn install_version_name(tv: &ToolVersion, cache_key: &str) -> String { if tv.version == "latest" || tv.version.is_empty() { - cache_key[..7.min(cache_key.len())].to_string() + Self::content_version_name(cache_key) } else { - tv.tv_pathname() + Self::sanitize_install_version_name(tv.tv_pathname()) + } + } + + fn content_version_name(cache_key: &str) -> String { + let short = &cache_key[..7.min(cache_key.len())]; + if short.is_empty() { + "_implicit".to_string() + } else { + short.to_string() + } + } + + fn sanitize_install_version_name(version_name: String) -> String { + match version_name.replace('\\', "-").as_str() { + "." => "_".to_string(), + ".." => "__".to_string(), + name => name.to_string(), } } @@ -555,15 +572,15 @@ impl HttpBackend { Ok(()) } - /// Create additional symlink for implicit versions (latest, empty) + /// Create additional symlink for latest versions fn create_version_alias_symlink(&self, tv: &ToolVersion, cache_key: &str) -> Result<()> { - if tv.version != "latest" && !tv.version.is_empty() { + if tv.version != "latest" { return Ok(()); } - let content_version = &cache_key[..7.min(cache_key.len())]; + let content_version = Self::content_version_name(cache_key); let original_path = tv.ba().installs_path.join(&tv.version); - let content_path = tv.ba().installs_path.join(content_version); + let content_path = tv.ba().installs_path.join(&content_version); if original_path.exists() { file::remove_all(&original_path)?; @@ -680,6 +697,40 @@ mod tests { assert!(!Path::new(&version_name).is_absolute()); } + #[test] + fn install_symlink_path_sanitizes_parent_version() { + let tv = http_test_tv(".."); + let version_name = HttpBackend::install_version_name(&tv, "abcdef123456"); + + assert_eq!(version_name, "__"); + assert!( + Path::new(&version_name) + .components() + .all(|c| matches!(c, std::path::Component::Normal(_))) + ); + } + + #[test] + fn install_symlink_path_sanitizes_windows_separators() { + let tv = http_test_tv(r"..\..\outside-root\mise-http-version-out\selected-prefix"); + let version_name = HttpBackend::install_version_name(&tv, "abcdef123456"); + + assert_eq!( + version_name, + "..-..-outside-root-mise-http-version-out-selected-prefix" + ); + assert!(!version_name.contains('\\')); + } + + #[test] + fn install_symlink_path_sanitizes_windows_unc_paths() { + let tv = http_test_tv(r"\\server\share"); + let version_name = HttpBackend::install_version_name(&tv, "abcdef123456"); + + assert_eq!(version_name, "--server-share"); + assert!(!version_name.contains('\\')); + } + #[test] fn latest_install_symlink_still_uses_content_version() { let tv = http_test_tv("latest"); @@ -687,6 +738,14 @@ mod tests { assert_eq!(version_name, "abcdef1"); } + + #[test] + fn empty_install_symlink_still_uses_content_version() { + let tv = http_test_tv(""); + let version_name = HttpBackend::install_version_name(&tv, "abcdef123456"); + + assert_eq!(version_name, "abcdef1"); + } } /// Returns install-time-only option keys for HTTP backend. From f6467adb36c62ff626c76cf740337512430a2038 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Sat, 6 Jun 2026 00:11:53 +0000 Subject: [PATCH 3/5] security: avoid http install name collisions --- src/backend/http.rs | 84 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 13 deletions(-) diff --git a/src/backend/http.rs b/src/backend/http.rs index bb925a63f5..83c227829e 100644 --- a/src/backend/http.rs +++ b/src/backend/http.rs @@ -506,14 +506,23 @@ impl HttpBackend { // Symlink creation // ------------------------------------------------------------------------- + /// Return the single path component used for the HTTP install symlink. fn install_version_name(tv: &ToolVersion, cache_key: &str) -> String { if tv.version == "latest" || tv.version.is_empty() { Self::content_version_name(cache_key) } else { - Self::sanitize_install_version_name(tv.tv_pathname()) + Self::sanitize_install_version_name(&tv.version, tv.tv_pathname()) } } + /// Return the absolute path where the HTTP install symlink should live. + fn install_path_for(tv: &ToolVersion, cache_key: &str) -> PathBuf { + tv.ba() + .installs_path + .join(Self::install_version_name(tv, cache_key)) + } + + /// Return a deterministic content-derived version name for implicit versions. fn content_version_name(cache_key: &str) -> String { let short = &cache_key[..7.min(cache_key.len())]; if short.is_empty() { @@ -523,11 +532,18 @@ impl HttpBackend { } } - fn sanitize_install_version_name(version_name: String) -> String { - match version_name.replace('\\', "-").as_str() { + /// Sanitize a requested version into a path component without collapsing identities. + fn sanitize_install_version_name(raw_version: &str, version_name: String) -> String { + let sanitized = match version_name.replace('\\', "-").as_str() { "." => "_".to_string(), ".." => "__".to_string(), name => name.to_string(), + }; + if sanitized == raw_version { + sanitized + } else { + let hash = hash::hash_sha256_to_str(raw_version); + format!("{}-{}", sanitized, &hash[..7]) } } @@ -542,8 +558,7 @@ impl HttpBackend { let cache_path = self.cache_path(cache_key); // Determine version name for install path - let version_name = Self::install_version_name(tv, cache_key); - let install_path = tv.ba().installs_path.join(&version_name); + let install_path = Self::install_path_for(tv, cache_key); // Clean up existing install if install_path.exists() { @@ -685,24 +700,33 @@ mod tests { ToolVersion::new(request, version.to_string()) } + fn version_hash(version: &str) -> String { + crate::hash::hash_sha256_to_str(version)[..7].to_string() + } + #[test] fn install_symlink_path_uses_sanitized_version_pathname() { - let tv = http_test_tv("/outside-root/mise-http-version-out/selected-prefix"); + let version = "/outside-root/mise-http-version-out/selected-prefix"; + let tv = http_test_tv(version); let version_name = HttpBackend::install_version_name(&tv, "abcdef123456"); assert_eq!( version_name, - "-outside-root-mise-http-version-out-selected-prefix" + format!( + "-outside-root-mise-http-version-out-selected-prefix-{}", + version_hash(version) + ) ); assert!(!Path::new(&version_name).is_absolute()); } #[test] fn install_symlink_path_sanitizes_parent_version() { - let tv = http_test_tv(".."); + let version = ".."; + let tv = http_test_tv(version); let version_name = HttpBackend::install_version_name(&tv, "abcdef123456"); - assert_eq!(version_name, "__"); + assert_eq!(version_name, format!("__-{}", version_hash(version))); assert!( Path::new(&version_name) .components() @@ -712,25 +736,49 @@ mod tests { #[test] fn install_symlink_path_sanitizes_windows_separators() { - let tv = http_test_tv(r"..\..\outside-root\mise-http-version-out\selected-prefix"); + let version = r"..\..\outside-root\mise-http-version-out\selected-prefix"; + let tv = http_test_tv(version); let version_name = HttpBackend::install_version_name(&tv, "abcdef123456"); assert_eq!( version_name, - "..-..-outside-root-mise-http-version-out-selected-prefix" + format!( + "..-..-outside-root-mise-http-version-out-selected-prefix-{}", + version_hash(version) + ) ); assert!(!version_name.contains('\\')); } #[test] fn install_symlink_path_sanitizes_windows_unc_paths() { - let tv = http_test_tv(r"\\server\share"); + let version = r"\\server\share"; + let tv = http_test_tv(version); let version_name = HttpBackend::install_version_name(&tv, "abcdef123456"); - assert_eq!(version_name, "--server-share"); + assert_eq!( + version_name, + format!("--server-share-{}", version_hash(version)) + ); assert!(!version_name.contains('\\')); } + #[test] + fn install_symlink_path_preserves_distinct_sanitized_versions() { + let slash = HttpBackend::install_version_name(&http_test_tv("a/b"), "abcdef123456"); + let colon = HttpBackend::install_version_name(&http_test_tv("a:b"), "abcdef123456"); + let backslash = HttpBackend::install_version_name(&http_test_tv(r"a\b"), "abcdef123456"); + let dash = HttpBackend::install_version_name(&http_test_tv("a-b"), "abcdef123456"); + + assert_eq!(dash, "a-b"); + assert_ne!(slash, dash); + assert_ne!(colon, dash); + assert_ne!(backslash, dash); + assert_ne!(slash, colon); + assert_ne!(slash, backslash); + assert_ne!(colon, backslash); + } + #[test] fn latest_install_symlink_still_uses_content_version() { let tv = http_test_tv("latest"); @@ -746,6 +794,15 @@ mod tests { assert_eq!(version_name, "abcdef1"); } + + #[test] + fn empty_install_path_uses_content_version_path() { + let tv = http_test_tv(""); + let install_path = HttpBackend::install_path_for(&tv, "abcdef123456"); + + assert_eq!(install_path, tv.ba().installs_path.join("abcdef1")); + assert_ne!(install_path, tv.ba().installs_path); + } } /// Returns install-time-only option keys for HTTP backend. @@ -890,6 +947,7 @@ impl Backend for HttpBackend { // Create symlinks self.create_install_symlink(&tv, &cache_key, &extraction_type, &opts)?; self.create_version_alias_symlink(&tv, &cache_key)?; + tv.install_path = Some(Self::install_path_for(&tv, &cache_key)); // Verify checksum for lockfile if lockfile_enabled || has_lockfile_checksum { From 39e8496d7f93ecd285a6a115efa0754ba9817828 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Sat, 6 Jun 2026 00:29:36 +0000 Subject: [PATCH 4/5] test(http): move tests after backend items --- src/backend/http.rs | 256 ++++++++++++++++++++++---------------------- 1 file changed, 128 insertions(+), 128 deletions(-) diff --git a/src/backend/http.rs b/src/backend/http.rs index 83c227829e..483be0f49b 100644 --- a/src/backend/http.rs +++ b/src/backend/http.rs @@ -677,134 +677,6 @@ impl HttpBackend { } } -#[cfg(test)] -mod tests { - use super::*; - use crate::cli::args::BackendResolution; - use crate::toolset::{ToolRequest, ToolSource}; - - fn http_test_tv(version: &str) -> ToolVersion { - let backend = Arc::new(BackendArg::new_raw( - "http-absolute-version".to_string(), - Some("http:absolute-version".to_string()), - "absolute-version".to_string(), - None, - BackendResolution::new(true), - )); - let request = ToolRequest::Version { - backend, - version: version.to_string(), - options: ToolVersionOptions::default(), - source: ToolSource::Argument, - }; - ToolVersion::new(request, version.to_string()) - } - - fn version_hash(version: &str) -> String { - crate::hash::hash_sha256_to_str(version)[..7].to_string() - } - - #[test] - fn install_symlink_path_uses_sanitized_version_pathname() { - let version = "/outside-root/mise-http-version-out/selected-prefix"; - let tv = http_test_tv(version); - let version_name = HttpBackend::install_version_name(&tv, "abcdef123456"); - - assert_eq!( - version_name, - format!( - "-outside-root-mise-http-version-out-selected-prefix-{}", - version_hash(version) - ) - ); - assert!(!Path::new(&version_name).is_absolute()); - } - - #[test] - fn install_symlink_path_sanitizes_parent_version() { - let version = ".."; - let tv = http_test_tv(version); - let version_name = HttpBackend::install_version_name(&tv, "abcdef123456"); - - assert_eq!(version_name, format!("__-{}", version_hash(version))); - assert!( - Path::new(&version_name) - .components() - .all(|c| matches!(c, std::path::Component::Normal(_))) - ); - } - - #[test] - fn install_symlink_path_sanitizes_windows_separators() { - let version = r"..\..\outside-root\mise-http-version-out\selected-prefix"; - let tv = http_test_tv(version); - let version_name = HttpBackend::install_version_name(&tv, "abcdef123456"); - - assert_eq!( - version_name, - format!( - "..-..-outside-root-mise-http-version-out-selected-prefix-{}", - version_hash(version) - ) - ); - assert!(!version_name.contains('\\')); - } - - #[test] - fn install_symlink_path_sanitizes_windows_unc_paths() { - let version = r"\\server\share"; - let tv = http_test_tv(version); - let version_name = HttpBackend::install_version_name(&tv, "abcdef123456"); - - assert_eq!( - version_name, - format!("--server-share-{}", version_hash(version)) - ); - assert!(!version_name.contains('\\')); - } - - #[test] - fn install_symlink_path_preserves_distinct_sanitized_versions() { - let slash = HttpBackend::install_version_name(&http_test_tv("a/b"), "abcdef123456"); - let colon = HttpBackend::install_version_name(&http_test_tv("a:b"), "abcdef123456"); - let backslash = HttpBackend::install_version_name(&http_test_tv(r"a\b"), "abcdef123456"); - let dash = HttpBackend::install_version_name(&http_test_tv("a-b"), "abcdef123456"); - - assert_eq!(dash, "a-b"); - assert_ne!(slash, dash); - assert_ne!(colon, dash); - assert_ne!(backslash, dash); - assert_ne!(slash, colon); - assert_ne!(slash, backslash); - assert_ne!(colon, backslash); - } - - #[test] - fn latest_install_symlink_still_uses_content_version() { - let tv = http_test_tv("latest"); - let version_name = HttpBackend::install_version_name(&tv, "abcdef123456"); - - assert_eq!(version_name, "abcdef1"); - } - - #[test] - fn empty_install_symlink_still_uses_content_version() { - let tv = http_test_tv(""); - let version_name = HttpBackend::install_version_name(&tv, "abcdef123456"); - - assert_eq!(version_name, "abcdef1"); - } - - #[test] - fn empty_install_path_uses_content_version_path() { - let tv = http_test_tv(""); - let install_path = HttpBackend::install_path_for(&tv, "abcdef123456"); - - assert_eq!(install_path, tv.ba().installs_path.join("abcdef1")); - assert_ne!(install_path, tv.ba().installs_path); - } -} - /// Returns install-time-only option keys for HTTP backend. pub fn install_time_option_keys() -> Vec { vec![ @@ -1003,3 +875,131 @@ impl Backend for HttpBackend { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::cli::args::BackendResolution; + use crate::toolset::{ToolRequest, ToolSource}; + + fn http_test_tv(version: &str) -> ToolVersion { + let backend = Arc::new(BackendArg::new_raw( + "http-absolute-version".to_string(), + Some("http:absolute-version".to_string()), + "absolute-version".to_string(), + None, + BackendResolution::new(true), + )); + let request = ToolRequest::Version { + backend, + version: version.to_string(), + options: ToolVersionOptions::default(), + source: ToolSource::Argument, + }; + ToolVersion::new(request, version.to_string()) + } + + fn version_hash(version: &str) -> String { + crate::hash::hash_sha256_to_str(version)[..7].to_string() + } + + #[test] + fn install_symlink_path_uses_sanitized_version_pathname() { + let version = "/outside-root/mise-http-version-out/selected-prefix"; + let tv = http_test_tv(version); + let version_name = HttpBackend::install_version_name(&tv, "abcdef123456"); + + assert_eq!( + version_name, + format!( + "-outside-root-mise-http-version-out-selected-prefix-{}", + version_hash(version) + ) + ); + assert!(!Path::new(&version_name).is_absolute()); + } + + #[test] + fn install_symlink_path_sanitizes_parent_version() { + let version = ".."; + let tv = http_test_tv(version); + let version_name = HttpBackend::install_version_name(&tv, "abcdef123456"); + + assert_eq!(version_name, format!("__-{}", version_hash(version))); + assert!( + Path::new(&version_name) + .components() + .all(|c| matches!(c, std::path::Component::Normal(_))) + ); + } + + #[test] + fn install_symlink_path_sanitizes_windows_separators() { + let version = r"..\..\outside-root\mise-http-version-out\selected-prefix"; + let tv = http_test_tv(version); + let version_name = HttpBackend::install_version_name(&tv, "abcdef123456"); + + assert_eq!( + version_name, + format!( + "..-..-outside-root-mise-http-version-out-selected-prefix-{}", + version_hash(version) + ) + ); + assert!(!version_name.contains('\\')); + } + + #[test] + fn install_symlink_path_sanitizes_windows_unc_paths() { + let version = r"\\server\share"; + let tv = http_test_tv(version); + let version_name = HttpBackend::install_version_name(&tv, "abcdef123456"); + + assert_eq!( + version_name, + format!("--server-share-{}", version_hash(version)) + ); + assert!(!version_name.contains('\\')); + } + + #[test] + fn install_symlink_path_preserves_distinct_sanitized_versions() { + let slash = HttpBackend::install_version_name(&http_test_tv("a/b"), "abcdef123456"); + let colon = HttpBackend::install_version_name(&http_test_tv("a:b"), "abcdef123456"); + let backslash = HttpBackend::install_version_name(&http_test_tv(r"a\b"), "abcdef123456"); + let dash = HttpBackend::install_version_name(&http_test_tv("a-b"), "abcdef123456"); + + assert_eq!(dash, "a-b"); + assert_ne!(slash, dash); + assert_ne!(colon, dash); + assert_ne!(backslash, dash); + assert_ne!(slash, colon); + assert_ne!(slash, backslash); + assert_ne!(colon, backslash); + } + + #[test] + fn latest_install_symlink_still_uses_content_version() { + let tv = http_test_tv("latest"); + let version_name = HttpBackend::install_version_name(&tv, "abcdef123456"); + + assert_eq!(version_name, "abcdef1"); + } + + #[test] + fn empty_install_symlink_still_uses_content_version() { + let tv = http_test_tv(""); + let version_name = HttpBackend::install_version_name(&tv, "abcdef123456"); + + assert_eq!(version_name, "abcdef1"); + } + + #[test] + fn empty_install_path_uses_content_version_path() { + let tv = http_test_tv(""); + let install_path = HttpBackend::install_path_for(&tv, "abcdef123456"); + + assert_eq!(install_path, tv.ba().installs_path.join("abcdef1")); + assert_ne!(install_path, tv.ba().installs_path); + } +} From 80ae49f22e665bf038fa24964819046f5de2ffd2 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Sat, 6 Jun 2026 00:39:39 +0000 Subject: [PATCH 5/5] security: align http install lookup paths --- src/backend/http.rs | 77 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 65 insertions(+), 12 deletions(-) diff --git a/src/backend/http.rs b/src/backend/http.rs index 483be0f49b..960406833c 100644 --- a/src/backend/http.rs +++ b/src/backend/http.rs @@ -13,6 +13,8 @@ use crate::config::Config; use crate::config::Settings; use crate::http::HTTP; use crate::install_context::InstallContext; +use crate::runtime_symlinks::is_runtime_symlink; +use crate::toolset::ToolRequest; use crate::toolset::ToolVersion; use crate::toolset::ToolVersionOptions; use crate::ui::progress_report::SingleReport; @@ -508,8 +510,10 @@ impl HttpBackend { /// Return the single path component used for the HTTP install symlink. fn install_version_name(tv: &ToolVersion, cache_key: &str) -> String { - if tv.version == "latest" || tv.version.is_empty() { + if tv.version == "latest" { Self::content_version_name(cache_key) + } else if tv.version.is_empty() { + "_implicit".to_string() } else { Self::sanitize_install_version_name(&tv.version, tv.tv_pathname()) } @@ -522,7 +526,21 @@ impl HttpBackend { .join(Self::install_version_name(tv, cache_key)) } - /// Return a deterministic content-derived version name for implicit versions. + /// Return the install path later lookups should check for this HTTP tool. + fn lookup_install_path(tv: &ToolVersion) -> PathBuf { + if let Some(path) = &tv.install_path { + return path.clone(); + } + if tv.version == "latest" { + tv.install_path() + } else { + tv.ba() + .installs_path + .join(Self::install_version_name(tv, "")) + } + } + + /// Return a deterministic content-derived version name for `latest` installs. fn content_version_name(cache_key: &str) -> String { let short = &cache_key[..7.min(cache_key.len())]; if short.is_empty() { @@ -830,6 +848,23 @@ impl Backend for HttpBackend { Ok(tv) } + fn is_version_installed( + &self, + _config: &Arc, + tv: &ToolVersion, + check_symlink: bool, + ) -> bool { + match tv.request { + ToolRequest::System { .. } => true, + _ => { + let install_path = Self::lookup_install_path(tv); + install_path.exists() + && !self.incomplete_file_path(tv).exists() + && (!check_symlink || !is_runtime_symlink(&install_path)) + } + } + } + async fn list_bin_paths( &self, _config: &Arc, @@ -837,18 +872,26 @@ impl Backend for HttpBackend { ) -> Result> { let raw_opts = tv.request.options(); let opts = HttpOptions::new(&raw_opts); - let install_path = tv.install_path(); + let install_path = Self::lookup_install_path(tv); + let mut tv = tv.clone(); + tv.install_path = Some(install_path.clone()); // Check for explicit bin_path if let Some(bin_path_template) = opts.bin_path() { - let bin_path = template_string(&bin_path_template, tv); - return Ok(vec![tv.runtime_path().join(bin_path)]); + let bin_path = template_string(&bin_path_template, &tv); + return Ok(vec![runtime_path_for_install_path( + &tv, + install_path.join(bin_path), + )]); } // Check for bin directory let bin_dir = install_path.join("bin"); if bin_dir.exists() { - return Ok(vec![tv.runtime_path().join("bin")]); + return Ok(vec![runtime_path_for_install_path( + &tv, + install_path.join("bin"), + )]); } // Search subdirectories for bin directories @@ -866,11 +909,11 @@ impl Backend for HttpBackend { } if paths.is_empty() { - Ok(vec![tv.runtime_path()]) + Ok(vec![runtime_path_for_install_path(&tv, install_path)]) } else { Ok(paths .into_iter() - .map(|path| runtime_path_for_install_path(tv, path)) + .map(|path| runtime_path_for_install_path(&tv, path)) .collect()) } } @@ -987,19 +1030,29 @@ mod tests { } #[test] - fn empty_install_symlink_still_uses_content_version() { + fn empty_install_symlink_uses_implicit_version() { let tv = http_test_tv(""); let version_name = HttpBackend::install_version_name(&tv, "abcdef123456"); - assert_eq!(version_name, "abcdef1"); + assert_eq!(version_name, "_implicit"); } #[test] - fn empty_install_path_uses_content_version_path() { + fn empty_install_path_uses_implicit_version_path() { let tv = http_test_tv(""); let install_path = HttpBackend::install_path_for(&tv, "abcdef123456"); - assert_eq!(install_path, tv.ba().installs_path.join("abcdef1")); + assert_eq!(install_path, tv.ba().installs_path.join("_implicit")); assert_ne!(install_path, tv.ba().installs_path); } + + #[test] + fn lookup_install_path_matches_sanitized_install_path() { + let version = "/outside-root/mise-http-version-out/selected-prefix"; + let tv = http_test_tv(version); + let install_path = HttpBackend::install_path_for(&tv, "abcdef123456"); + let lookup_path = HttpBackend::lookup_install_path(&tv); + + assert_eq!(lookup_path, install_path); + } }